273 Commits

Author SHA1 Message Date
3b7e1615c2 Merge pull request 'fix(jenkins): preflight node toolchain for database import/export' (#24) from master into release
Reviewed-on: #24
2026-05-15 17:21:29 +08:00
49ffa6b901 fix(jenkins): preflight node toolchain for database import/export 2026-05-15 17:10:07 +08:00
34dca5fccc Merge pull request 'master' (#22) from master into release
Reviewed-on: #22
2026-05-15 11:55:37 +08:00
kdletters
7a3b137565 Refactor local dev stack scheduler 2026-05-15 11:52:51 +08:00
0152f9bd67 Merge branch 'hermes/wechat'
# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md
#	docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md
#	server-rs/crates/module-runtime/src/errors.rs
#	src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
#	src/components/rpg-entry/RpgEntryHomeView.tsx
2026-05-15 11:32:51 +08:00
bb60ca91ef Match3D & Puzzle: runtime UI, assets, drag fix
Backend: stop treating background music as a required draft asset and remove auto-submit/plan for background music; load persisted generated UI/assets into Match3D agent session responses (added helpers to resolve profile id and fetch existing generated assets). Frontend: make Match3D result preview reuse runtime UI styles, unify runtime settings entry, update PuzzleRuntime to apply immediate pointermove transforms (disable drag transition), use SVG clipPath for merged piece rounding, ensure PuzzleRuntimeShell supplies platform theme classes, and adjust related tests. Docs & logs: update decision log, pitfalls and product docs to reflect these changes.
2026-05-15 08:49:59 +08:00
c94f22e26c feat: gate recharge payment by login device 2026-05-15 08:43:21 +08:00
5b70ec6af7 feat: 接入微信H5与Native充值支付 2026-05-15 06:40:40 +08:00
0f36beee91 Merge branch 'master' of https://git.genarrative.world/GenarrativeAI/Genarrative 2026-05-15 06:36:48 +08:00
3cb3efb4d0 Prune stale docs and update .hermes content
Delete a large set of outdated documentation (many files under docs/ and .hermes/plans/, including audits, design, prd, technical, planning, assets, and todos). Update and consolidate .hermes content: refresh shared-memory pages (decision-log, development-workflow, document-map, pitfalls, project-overview, team-conventions) and several skills/references under .hermes/skills. Also modify AGENTS.md, README.md, UI_CODING_STANDARD.md, docs/README.md and .encoding-check-ignore. Purpose: clean up stale planning/audit material and keep current hermes documentation and related top-level docs in sync.
2026-05-15 06:24:07 +08:00
c7fe793a9e feat: 支持充值商品配置和档位首充 2026-05-15 06:20:52 +08:00
9c33cc565c Merge pull request 'refactor: modularize api server assets and handlers' (#23) from hermes/hermes-4fd30995 into master
Reviewed-on: #23
2026-05-15 06:01:14 +08:00
5633059f3d Merge branch 'master' into hermes/hermes-4fd30995 2026-05-15 05:58:16 +08:00
7972661d1e fix: 对齐首充双倍展示状态 2026-05-15 03:59:37 +08:00
b97a3ccebd Merge origin/master into hermes/hermes-4fd30995 2026-05-15 03:41:50 +08:00
6672867c6f Add WeChat Pay local skills 2026-05-15 03:35:30 +08:00
2eded08bc7 Merge branch 'master' of https://git.genarrative.world/GenarrativeAI/Genarrative 2026-05-15 02:41:52 +08:00
3288b6aafb Merge branch 'master' of https://git.genarrative.world/GenarrativeAI/Genarrative 2026-05-15 02:41:43 +08:00
c9a4176a41 fix(jenkins): bound production git fetch refspec 2026-05-15 02:41:09 +08:00
74fd9a33ac Increase VectorEngine timeouts and add image UI
Add VectorEngine image generation config and raise request timeouts (env + scripts) from 180000 to 1000000ms. Introduce a reusable CreativeImageInputPanel component with tests and wire up mobile keyboard-focus helpers; update generation views and related tests (CustomWorldGenerationView, BarkBattle editor, Match3D, Puzzle flows). Improve API error handling / VectorEngine request guidance (packages/shared http.ts and docs), and apply multiple backend/frontend fixes for puzzle/match3d/prompt handling. Also include extensive docs and decision-log updates describing UI/UX decisions and verification steps.
2026-05-15 02:40:59 +08:00
a30f296cdc Merge remote-tracking branch 'origin/hermes/wechat' 2026-05-15 01:36:24 +08:00
73424f958a Merge remote-tracking branch 'origin/master' into hermes/wechat
# Conflicts:
#	.hermes/shared-memory/pitfalls.md
#	.hermes/todos/【后端架构】api-server能力模块化与图片资产Adapter收口计划-2026-05-14.md
2026-05-15 01:28:04 +08:00
f31bb7e7e5 Add SpacetimeDB schema guard 2026-05-15 01:25:56 +08:00
ed8c93fb5d fix: 刷新微信支付到账泥点 2026-05-15 01:19:34 +08:00
8ade75390c Persist api-server logs and refresh recharge balance 2026-05-15 01:07:39 +08:00
2801b55d2f fix: 修复微信支付回跳刷新与查单确认 2026-05-14 23:52:01 +08:00
1b54db4f92 refactor: modularize api server assets and handlers 2026-05-14 22:54:52 +08:00
b24af5a279 chore: stop tracking local spacetime config 2026-05-14 21:55:21 +08:00
4642855fd0 Merge branch 'master' of https://git.genarrative.world/GenarrativeAI/Genarrative 2026-05-14 21:33:34 +08:00
cf3dcc6195 fix mini program payment bridge 2026-05-14 21:12:37 +08:00
bca439726d fix wechat pay request headers 2026-05-14 20:51:32 +08:00
548db78ca7 Update Match3D/image-generation docs & code
Adds/updates documentation, assets and implementation for Match3D and puzzle image generation workflows. Key changes: decision logs and pitfalls updated to prefer VectorEngine Gemini for Match3D material sheets and to require edits (multipart) for 1:1 container reference images; guidance added for when to use APIMart vs VectorEngine. .env.example clarified APIMart/Responses config. Many new public assets and PPT visuals added. Code changes across frontend and backend: updated shared contracts, server-rs match3d/puzzle/image-generation handlers, VectorEngine/OpenAI image generation clients, and multiple React components/tests to handle UI/background/container image signing, edits workflow, and puzzle UI background resolution. Added src/services/puzzle-runtime/puzzleUiBackgroundSource.ts and related test updates. Includes notes about multipart HTTP/1.1 requirement and test/verification commands in docs.
2026-05-14 20:34:45 +08:00
5c5a8d4a40 fix: enforce WeChat Pay JSAPI field limits 2026-05-14 20:22:49 +08:00
514365fdec fix(admin): decode recharge order enum cells 2026-05-14 20:15:51 +08:00
de25324991 fix(admin): decode recharge order enum cells 2026-05-14 20:14:25 +08:00
1c35662ed5 删除多余文件 2026-05-14 20:14:24 +08:00
379ce60839 docs: add shared todo plan naming rules 2026-05-14 20:13:13 +08:00
4f36235f60 Merge pull request 'feat: wire bark battle platform loop' (#21) from hermes/hermes-1e775b03 into master
Reviewed-on: #21
2026-05-14 19:18:20 +08:00
e55c12b68b style: apply rustfmt after master merge 2026-05-14 19:17:40 +08:00
502811a103 Merge remote-tracking branch 'origin/master' into hermes/hermes-1e775b03
# Conflicts:
#	server-rs/crates/api-server/src/app.rs
#	server-rs/crates/api-server/src/creation_entry_config.rs
#	server-rs/crates/api-server/src/puzzle.rs
#	server-rs/crates/spacetime-client/src/lib.rs
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
2026-05-14 19:17:17 +08:00
1d7ef7e4b6 feat: wire bark battle platform loop
Some checks are pending
CI / verify (pull_request) Waiting to run
2026-05-14 18:20:46 +08:00
cb794601be docs: expand api-server modularization plan 2026-05-14 16:16:37 +08:00
4ba1ebbbdf docs: expand api-server modularization plan 2026-05-14 16:12:28 +08:00
332f887c66 docs: add shared todo plan naming rules 2026-05-14 15:45:25 +08:00
e444266e1e feat: add edutainment drawing and visual package flows 2026-05-14 14:56:36 +08:00
10e8beea80 删除多余文件 2026-05-14 14:54:21 +08:00
d33c937ebc 1 2026-05-14 14:21:17 +08:00
7a75f5d612 Merge branch 'master' of https://git.genarrative.world/GenarrativeAI/Genarrative 2026-05-14 13:42:12 +08:00
2dc9d752e4 1 2026-05-14 13:40:50 +08:00
166544fae6 Persist auth store into formal tables 2026-05-14 03:59:55 +08:00
e8d832c1ea Merge pull request 'fix: 修复微信支付生产构建依赖' (#20) from master into release
Reviewed-on: #20
2026-05-14 03:38:41 +08:00
e8648e45fc fix: 修复微信支付生产构建依赖 2026-05-14 02:40:34 +08:00
b08f878841 Merge pull request 'master' (#19) from master into release
Reviewed-on: #19
2026-05-14 01:25:03 +08:00
5a55180b78 1 2026-05-14 01:11:58 +08:00
ae58a443a3 feat: 接入微信小程序支付 2026-05-14 00:16:17 +08:00
bf4423e53b Add release web artifact rsync fallback 2026-05-13 21:22:26 +08:00
57de9a8df6 Merge pull request 'hermes/visual-novel-genarrative' (#18) from hermes/visual-novel-genarrative into master
Reviewed-on: #18
2026-05-13 21:19:38 +08:00
c1131e6f55 feat: add visual novel AI image entry points
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-13 21:14:13 +08:00
2a75a19ece fix: handle visual novel typed SSE events 2026-05-13 20:44:22 +08:00
5b96265c50 fix wechat mini program phone parsing 2026-05-13 20:39:01 +08:00
ad1b91498d Merge pull request 'master' (#17) from master into release
Reviewed-on: #17
2026-05-13 20:26:33 +08:00
2277b37888 Limit Jenkins fallback git checkouts 2026-05-13 20:24:58 +08:00
be53a90f77 remove github ci 2026-05-13 19:40:33 +08:00
71c7dd2558 Merge pull request 'Use domain fallback for Jenkins git checkout' (#16) from master into release
Reviewed-on: #16
2026-05-13 19:38:54 +08:00
bcd7617fb7 Use domain fallback for Jenkins git checkout
Some checks failed
CI / verify (push) Has been cancelled
CI / verify (pull_request) Has been cancelled
2026-05-13 19:35:50 +08:00
a6de4d8a32 Merge branch 'master' into release 2026-05-13 17:34:46 +08:00
49468441bc fix(jenkins): use git domain for scm remotes
Some checks failed
CI / verify (push) Has been cancelled
2026-05-13 17:17:55 +08:00
8ddaf72eb7 Merge pull request 'master' (#15) from master into release
Reviewed-on: #15
2026-05-13 16:21:57 +08:00
a92dc2b7b0 fix(jenkins): add git fallback and nginx aliases
Some checks failed
CI / verify (push) Has been cancelled
CI / verify (pull_request) Has been cancelled
2026-05-13 16:07:54 +08:00
4fecf9c975 fix(auth): tighten refresh session revocation 2026-05-13 15:13:43 +08:00
ad970797e9 Merge pull request 'master' (#14) from master into release
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/14
2026-05-13 13:23:06 +08:00
c3fbf7a30b feat: tighten visual novel one-line generation flow 2026-05-13 12:26:39 +08:00
b13870f71b 1
Some checks failed
CI / verify (push) Has been cancelled
CI / verify (pull_request) Has been cancelled
2026-05-13 03:11:00 +08:00
e4a8bd42bb Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative 2026-05-13 03:10:55 +08:00
01c5ab985a 1 2026-05-13 00:28:07 +08:00
ac12f1ed5e Merge branch 'codex/wechat'
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 22:30:50 +08:00
e36a562098 feat: support mini program phone authorization binding 2026-05-12 22:30:24 +08:00
36e134e323 merge codex/wechat into master
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 18:58:21 +08:00
26139f80d3 test: add wechat miniprogram auth smoke 2026-05-12 18:57:27 +08:00
9b72dbb3ea ci: load nginx dynamic modules for brotli probe
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 16:59:01 +08:00
188c6704db ci: detect nginx brotli via config test
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 16:53:53 +08:00
d641840098 ci: enable nginx compression in server provision
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 16:30:35 +08:00
aec9142481 Merge origin/master into codex/wechat 2026-05-12 16:20:45 +08:00
d41f260a2a feat: add baby object match edutainment flow
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 16:08:59 +08:00
cf074837a4 docs: ignore local load test artifacts
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 15:14:11 +08:00
ed7a6f48d0 Merge pull request 'hermes/hermes-1e775b03' (#13) from hermes/hermes-1e775b03 into master
Some checks failed
CI / verify (push) Has been cancelled
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/13
2026-05-12 15:11:49 +08:00
8c6ec9e6e4 Merge remote-tracking branch 'origin/master' into hermes/hermes-1e775b03
Some checks failed
CI / verify (pull_request) Has been cancelled
# Conflicts:
#	docs/technical/README.md
#	src/components/custom-world-home/CustomWorldCreationHub.tsx
#	src/components/custom-world-home/creationWorkShelf.ts
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx
2026-05-12 15:02:47 +08:00
33c9079d3b feat: complete bark battle playable demo 2026-05-12 14:42:58 +08:00
7b4ba61b4d Merge remote-tracking branch 'origin/master' into hermes/hermes-3337436a
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 14:18:30 +08:00
ea0b67a951 docs: add security vulnerability scan report 2026-05-12 14:17:23 +08:00
4dfa8452db docs: add disaster recovery plan draft
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 14:13:53 +08:00
22810245f5 refactor match3d runtime adapters 2026-05-12 14:02:42 +08:00
eb76bfc031 Merge remote-tracking branch 'origin/master'
Some checks failed
CI / verify (push) Has been cancelled
2026-05-12 13:59:28 +08:00
183e78d475 perf: batch recent play counts for gallery lists 2026-05-12 10:59:51 +08:00
612d105a23 fix: resolve k6 loadtest data path 2026-05-11 22:18:43 +08:00
b994acf635 test: add k6 works list load test 2026-05-11 21:31:24 +08:00
ef4f91a75e 1
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 20:57:16 +08:00
481a27fc53 1 2026-05-11 20:27:41 +08:00
54968701f0 fix public work detail not found recovery
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 19:52:25 +08:00
5cb5329f4e feat: add bark battle debug feedback 2026-05-11 18:32:02 +08:00
2b046656dc feat: add bark battle browser prototype 2026-05-11 18:01:55 +08:00
7cea41c911 Add frontend debug mode gate 2026-05-11 18:00:36 +08:00
928acb4302 Extend sccache startup timeout for Windows builds 2026-05-11 17:24:24 +08:00
bf72c2e48d docs: add bark battle backend BDD and TDD 2026-05-11 16:17:22 +08:00
e30b733b17 1 2026-05-11 16:15:48 +08:00
fa61eeb0b0 docs: add bark battle BDD acceptance scenarios 2026-05-11 16:10:48 +08:00
2ca096f821 docs: add bark battle backend ddd plan 2026-05-11 15:52:20 +08:00
2b6087de4c docs: add bark battle 2d runtime plan 2026-05-11 15:21:51 +08:00
fda996031f docs: add BDD skill
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 14:29:53 +08:00
10ed4fa051 docs: clarify SpacetimeDB root-dir usage
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 14:27:33 +08:00
ac2cf78ffa Fix SpacetimeDB wasm dependency boundary
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 13:47:26 +08:00
5edfb756c7 merge: share game-studio hermes plugin
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 12:56:36 +08:00
0c9254502c merge: admin creation entry switches
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 12:02:41 +08:00
0461c0ee41 feat: add admin creation entry switches
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-11 12:02:39 +08:00
81f57ea5ce chore: share game-studio hermes plugin 2026-05-11 12:00:45 +08:00
d23cf3807d fix: restore match3d bad gateway helper after merge
Some checks failed
CI / verify (push) Has been cancelled
2026-05-11 11:35:59 +08:00
6c1579a786 merge: database backed creation entry config
# Conflicts:
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
2026-05-11 11:25:35 +08:00
793d82cccd feat: move creation entry config to database 2026-05-11 11:23:24 +08:00
5cc8293380 feat: add child motion picture book stage tooling
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 23:10:24 +08:00
85ed8ca90c 1
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 22:28:47 +08:00
d0a9348e72 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative 2026-05-10 22:28:43 +08:00
192accd796 1 2026-05-10 22:20:54 +08:00
54c2d6de47 feat: connect child motion warmup to mocap input
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 21:22:50 +08:00
7f2461313e Merge remote-tracking branch 'origin/master' into hermes/hermes-822e6126
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 20:04:13 +08:00
f74717c415 fix(dev): resolve local stack ports before startup
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 20:00:42 +08:00
75bca28191 feat: smooth mocap palm cursor 2026-05-10 18:51:43 +08:00
46a254f142 feat: add child motion entry and fix auth env
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 18:27:51 +08:00
86fc382413 feat: add shared runtime input device layer
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 17:50:00 +08:00
643161a168 fix(admin): populate tracking event key options
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 14:48:10 +08:00
d6219f1a0c fix(dev): precheck dev ports and avoid pid file locks
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 14:02:36 +08:00
35d63f5b2e Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 13:19:03 +08:00
1c16152708 1 2026-05-10 13:18:46 +08:00
f6084d0910 Merge pull request 'feat: add mocap puzzle debug and drag support' (#11) from hermes/hermes-da285bce into master
Some checks failed
CI / verify (push) Has been cancelled
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/11
2026-05-10 12:37:38 +08:00
6ed6859855 feat: add mocap puzzle debug and drag support
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-10 12:34:18 +08:00
kdletters
9b39a52049 docs: record local spacetime auth recovery
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 04:37:33 +08:00
kdletters
fc54bff62f Keep local Rust dev builds on debug
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 04:29:20 +08:00
1767bed609 docs: add motion demo entry and warmup specs
Some checks failed
CI / verify (push) Has been cancelled
2026-05-10 00:01:27 +08:00
dada5a4797 Document local SpacetimeDB dev skip and clear workflows
Some checks failed
CI / verify (push) Has been cancelled
2026-05-09 20:38:48 +08:00
7e608d4230 fix: ensure analytics date dimension for tracking events
Some checks failed
CI / verify (push) Has been cancelled
2026-05-09 20:10:18 +08:00
3ad1075227 feat: add work-level play tracking
Some checks failed
CI / verify (push) Has been cancelled
2026-05-09 19:57:22 +08:00
32a1530ab1 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-05-09 19:56:43 +08:00
7c8aa1e124 1 2026-05-09 19:56:03 +08:00
641d91cf11 feat: reopen match3d creation entry
Some checks failed
CI / verify (push) Has been cancelled
2026-05-09 18:39:47 +08:00
052dbc248b Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-05-09 18:24:41 +08:00
bc704d0c22 1 2026-05-09 18:24:08 +08:00
a0ed128bde 1 2026-05-09 17:15:23 +08:00
80a4183b45 fix recommend runtime auth isolation 2026-05-09 16:08:40 +08:00
8669a996ca Polish admin table labels and button layout
Some checks failed
CI / verify (push) Has been cancelled
2026-05-09 12:47:27 +08:00
9ca66715a4 fix auth login state race 2026-05-09 01:03:56 +08:00
e390b72a0c Guard optional Jenkins vars in database import and export
Some checks failed
CI / verify (push) Has been cancelled
2026-05-08 23:26:14 +08:00
cf9fb5ac40 Add bootstrap secret flow to production Stdb builds
Some checks failed
CI / verify (push) Has been cancelled
2026-05-08 22:58:09 +08:00
a1e5c2150c feat: refine match3d spawn visuals
Some checks failed
CI / verify (push) Has been cancelled
2026-05-08 22:52:26 +08:00
23ba2703b4 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-05-08 22:12:10 +08:00
96df12cd15 1 2026-05-08 22:07:08 +08:00
65c2b8cd79 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative 2026-05-08 22:07:05 +08:00
199b44c18c Add backend feedback submission and image preview
Some checks failed
CI / verify (push) Has been cancelled
2026-05-08 21:47:45 +08:00
e410f7974e 1 2026-05-08 21:46:11 +08:00
94975e4735 1 2026-05-08 20:48:29 +08:00
b2ac92e0fc Merge pull request 'add row data inspect in admin' (#10) from hermes/hermes-f32d3246 into master
Some checks failed
CI / verify (push) Has been cancelled
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/10
2026-05-08 20:17:39 +08:00
edcdc01e43 fix(dev): publish spacetime module from server workspace
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 19:17:39 +08:00
621bf6506c fix(dev): accept spacetime port fallback
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 18:15:48 +08:00
e226c39b2c add .worktreeinclude file
Some checks failed
CI / verify (push) Has been cancelled
2026-05-08 17:58:33 +08:00
26a3c89d1d fix(dev): use local spacetime data dir
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 17:57:03 +08:00
7e35231dfe docs: sync genarrative shared skills
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 17:39:49 +08:00
3efc646868 fix(dev): repair rust stack startup 2026-05-08 17:29:22 +08:00
747ef790ac docs: add shared admin backoffice skill 2026-05-08 16:58:28 +08:00
b995809f75 feat(admin): refine table query tools 2026-05-08 16:50:35 +08:00
72fce47187 feat(admin): add database table query page 2026-05-08 16:39:44 +08:00
63aaf1ecaf Merge pull request 'hermes/hermes-996d586b' (#9) from hermes/hermes-996d586b into master
Some checks failed
CI / verify (push) Has been cancelled
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/9
2026-05-08 14:46:55 +08:00
1f6ac7dddd Merge branch 'master' into hermes/hermes-996d586b
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 14:46:30 +08:00
c55745f70b Merge pull request '添加反馈入口' (#8) from hermes/hermes-19e77eb0 into master
Some checks failed
CI / verify (push) Has been cancelled
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/8
2026-05-08 14:43:06 +08:00
5e419fa2f7 Merge remote-tracking branch 'origin/master' into hermes/hermes-996d586b
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 14:42:15 +08:00
539ddbde24 ignore .env.secrets.local 2026-05-08 14:41:11 +08:00
e694c6605a fix: trigger login tracking on session restore 2026-05-08 14:36:56 +08:00
326cc6b062 Merge pull request '修复oss签名时间格式错误' (#7) from hermes/hermes-15235e5a into master
Some checks failed
CI / verify (push) Has been cancelled
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/7
2026-05-08 14:34:35 +08:00
7e8cc22859 fix oss object key guidance
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 14:28:04 +08:00
20d5121c6c fix oss bucket-prefixed signing 2026-05-08 14:20:56 +08:00
91d993dc6b feat: record daily login tracking on session refresh 2026-05-08 14:08:22 +08:00
f343555a19 feat: record daily login tracking on auth success 2026-05-08 14:00:04 +08:00
4aa127cd18 test: cover profile feedback route and view
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-08 12:27:14 +08:00
78ce8527fc feat: wire profile feedback navigation 2026-05-08 12:25:08 +08:00
424e75a922 feat: add feedback shortcut to profile tab 2026-05-08 12:23:37 +08:00
7eb531ccca feat: add platform feedback view 2026-05-08 12:21:55 +08:00
3b0dd2ebeb feat: add profile feedback route stage 2026-05-08 12:20:32 +08:00
b5c8ec304f docs: add profile feedback entry prd 2026-05-08 12:18:54 +08:00
104e19d257 Merge remote-tracking branch 'origin/master' into hermes/hermes-19e77eb0 2026-05-08 12:15:51 +08:00
0b71fa8eb0 docs: add profile feedback implementation plan 2026-05-08 12:14:49 +08:00
98be6eb0e4 fix: compile daily login tracking procedure 2026-05-08 11:55:08 +08:00
abf1f1ebea 1 2026-05-08 11:44:42 +08:00
bdded3d708 fix: stabilize admin tracking event display 2026-05-08 11:30:14 +08:00
b08127031c Merge branch 'hermes/hermes-996d586b'
Some checks failed
CI / verify (push) Has been cancelled
2026-05-08 11:23:57 +08:00
0235200d32 fix: stabilize admin tracking event display 2026-05-08 11:15:42 +08:00
e8fee0172a feat: add puzzle onboarding and match3d entry updates
Some checks failed
CI / verify (push) Has been cancelled
2026-05-07 23:30:54 +08:00
a71df45437 Merge remote-tracking branch 'origin/master' into hermes/hermes-996d586b 2026-05-07 21:16:21 +08:00
df80876f60 Consolidate workspace deps and migrate sha1 to sha2
Some checks failed
CI / verify (push) Has been cancelled
2026-05-07 21:11:14 +08:00
9146e5b8ec 优化邀请码链接自动打开流程
Some checks failed
CI / verify (push) Has been cancelled
2026-05-07 17:09:35 +08:00
fd16485827 feat: add admin tracking events export 2026-05-07 17:02:31 +08:00
89be59d701 Merge pull request 'chore(api-server): 外部模型与网关配置改为环境变量' (#6) from hermes/hermes-996d586b into master
Some checks failed
CI / verify (push) Has been cancelled
Reviewed-on: http://82.157.175.59:3000/GenarrativeAI/Genarrative/pulls/6
2026-05-07 16:48:41 +08:00
59ef2ab472 chore(env): 补齐本地外部模型配置
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-07 16:34:01 +08:00
13547091ca chore(api-server): 外部模型与网关配置改为环境变量 2026-05-07 15:59:00 +08:00
6a830b349b ci: move server provision logic to script
Some checks failed
CI / verify (push) Has been cancelled
2026-05-07 15:15:00 +08:00
d5d3fa1641 ci: fix server provision shell quoting
Some checks failed
CI / verify (push) Has been cancelled
2026-05-07 14:57:51 +08:00
0816d2e326 ci: quote server provision bash heredocs
Some checks failed
CI / verify (push) Has been cancelled
2026-05-07 14:52:18 +08:00
dcd5201bb3 ci: harden maintenance and api deploy checkout
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 18:08:01 +08:00
cc9d289310 ci: default api deploy env params in shell
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 17:39:54 +08:00
3111e22288 ci: escape regex dollar in Jenkins messages
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 17:29:14 +08:00
014e88afaa ci: allow api env update via sudo
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 17:23:06 +08:00
cf27686e17 ci: propagate database to api deploy
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 17:03:12 +08:00
ca394766d1 ci: align stdb archive build name
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 16:33:43 +08:00
e9dfcda418 ci: fix git bash path literals
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 16:05:31 +08:00
b27424105d ci: run stdb build with git bash
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 16:02:05 +08:00
7364d0f2d3 ci: fix stdb path normalization regex
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 15:55:03 +08:00
b25b14a329 ci: use msys paths for stdb cargo env
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 15:49:45 +08:00
950a785213 ci: avoid cygpath in stdb windows bash
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 15:38:31 +08:00
95a2adbdc0 ci: remove stdb release build name parameter
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 15:23:43 +08:00
ceea868478 ci: quote stdb bash build version
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 15:19:19 +08:00
bc0ebe3c25 ci: pass stdb cargo env into bash
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 15:16:14 +08:00
3ccbe6fe77 ci: fix stdb production build env
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 14:51:23 +08:00
d31a28178e ci: shallow clone production Jenkins checkouts
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 14:23:48 +08:00
96e1d7c4fb ci: split production build Jenkins nodes
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 14:06:12 +08:00
kdletters
0fae3319a9 删除过时文档
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 13:03:05 +08:00
kdletters
d06107f2c6 落地方洞挑战图片与运行态交互
Some checks failed
CI / verify (push) Has been cancelled
2026-05-06 12:52:47 +08:00
kdletters
60b667a9d1 Extend square-hole creation flow with visual asset timeout guard 2026-05-06 12:52:47 +08:00
2252afb080 fix: improve match3d tray preview readability
Some checks failed
CI / verify (push) Has been cancelled
2026-05-05 17:17:27 +08:00
06b8b46530 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-05-05 14:58:07 +08:00
07e777fef8 1 2026-05-05 14:40:41 +08:00
kdletters
995661e7cc Preserve partial creation replies on stream failure
Some checks failed
CI / verify (push) Has been cancelled
2026-05-05 11:31:50 +08:00
100fee7e7a docs: add genarrative play type skill
Some checks failed
CI / verify (push) Has been cancelled
2026-05-04 16:34:18 +08:00
1d9d8c2e41 feat: add analytics metric granularity query
Some checks failed
CI / verify (push) Has been cancelled
2026-05-04 16:29:48 +08:00
44d9bd55de docs: initialize shared Hermes project memory 2026-05-04 16:29:48 +08:00
8692dbad85 Align build agent remoting workdir docs
Some checks failed
CI / verify (push) Has been cancelled
2026-05-04 14:33:40 +08:00
5c7c039e52 feat: add analytics date dimension bindings
Some checks failed
CI / verify (push) Has been cancelled
- lock profile task tracking scope to user

- add analytics date dimension module support and tests

- regenerate SpacetimeDB Rust bindings with private APIs
2026-05-04 13:54:41 +08:00
9f3e34e81a feat: add invite code validity controls
- Add invite code starts/expires fields across contracts, API, Spacetime bindings, and admin UI
- Enforce pending/expired invite code redemption behavior and expose admin status
- Add admin write-operation confirmation guard and documentation
- Add invite code contract/runtime tests
2026-05-04 13:54:40 +08:00
1142e90a35 Document build agent inbound self recovery
Some checks failed
CI / verify (push) Has been cancelled
2026-05-04 05:13:15 +08:00
Hermes Agent
e847fcea6f Merge remote-tracking branch 'origin/master' 2026-05-04 03:02:49 +08:00
Hermes Agent
46d240e37d feat: add custom world opening cg contract 2026-05-04 03:02:24 +08:00
45719e7650 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-05-04 02:33:15 +08:00
34aecdddf1 Add skill for gameplay entry type workflows 2026-05-04 02:32:38 +08:00
f1e86a88da feat: refine match3d brick runtime assets
Some checks failed
CI / verify (push) Has been cancelled
2026-05-03 23:26:08 +08:00
ce98a29c4d feat: add wechat miniprogram webview login 2026-05-03 19:05:45 +08:00
9baa515a75 Add WeChat miniprogram web-view shell 2026-05-03 16:29:42 +08:00
49aad7311c Add local Jenkins controller watchdog
Some checks failed
CI / verify (push) Has been cancelled
2026-05-03 14:18:27 +08:00
01b302d7eb Add resilient Jenkins inbound agent setup
Some checks failed
CI / verify (push) Has been cancelled
2026-05-03 14:01:19 +08:00
8e6d1971ea fix: temporarily restore public api proxy
Some checks failed
CI / verify (push) Has been cancelled
2026-05-03 12:16:26 +08:00
fecac5344a Merge pull request #5 from codex/publish-flow
Some checks failed
CI / verify (push) Has been cancelled
Harden production publish flow
2026-05-03 03:47:13 +08:00
27342a8cca Merge remote-tracking branch 'origin/master' into codex/publish-flow
Some checks failed
CI / verify (pull_request) Has been cancelled
# Conflicts:
#	docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md
#	docs/technical/README.md
#	jenkins/Jenkinsfile.database-export
#	jenkins/Jenkinsfile.database-import
2026-05-03 03:46:39 +08:00
cc38057c3c chore: harden spacetime publish provisioning
Some checks failed
CI / verify (pull_request) Has been cancelled
2026-05-03 03:38:10 +08:00
62afaf620a fix: publish stdb with service identity 2026-05-03 02:11:44 +08:00
562b5eb720 fix: avoid root spacetime health checks 2026-05-03 02:01:16 +08:00
e0d0531c9c fix: sync spacetimedb binaries during provision 2026-05-03 01:56:24 +08:00
96f13bdfed fix: avoid production spacetimedb port conflict 2026-05-03 00:49:36 +08:00
d39ac86c27 Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
Some checks failed
CI / verify (push) Has been cancelled
2026-05-03 00:18:09 +08:00
801d1d534a 1 2026-05-03 00:17:50 +08:00
019c8a2b03 Keep web artifacts on build agent 2026-05-03 00:10:43 +08:00
4358f38259 Install sccache during server provision 2026-05-02 23:56:28 +08:00
98c83478ad Run notification pipeline without build agent 2026-05-02 23:14:57 +08:00
36c81c30be Avoid logging notification recipient secrets 2026-05-02 23:11:27 +08:00
813dbf1fdd Persist notification recipients in Jenkins credentials 2026-05-02 22:47:55 +08:00
39b1141287 Add production email notification pipeline 2026-05-02 22:35:36 +08:00
ace13c7047 Install production build toolchain in provision 2026-05-02 22:10:01 +08:00
73d5ef40ed Isolate Jenkins cargo environment 2026-05-02 21:56:07 +08:00
6c519970b4 Add development HTTP nginx provision mode 2026-05-02 21:20:27 +08:00
e61b1a1586 Split production cargo caches by component 2026-05-02 21:18:59 +08:00
d48916157b Make production builds bootstrap agent dependencies 2026-05-02 20:42:47 +08:00
7160e90909 Guard server provision nginx install 2026-05-02 20:25:01 +08:00
64fda2a677 Handle empty commit hash in Jenkins checkout 2026-05-02 20:01:35 +08:00
0c9e58b75a Enable Jenkins global timestamps for production pipelines 2026-05-02 19:43:17 +08:00
06d6f7716e Use compatible timestamp wrapper in Jenkins pipelines 2026-05-02 19:34:11 +08:00
a53167c872 Keep timestamped production Jenkins logs 2026-05-02 19:30:41 +08:00
23cb37c18a Revert "Remove unsupported Jenkins timestamp option"
This reverts commit ae33f10f17.
2026-05-02 19:29:58 +08:00
ae33f10f17 Remove unsupported Jenkins timestamp option 2026-05-02 19:28:07 +08:00
bdc3257003 Add production Jenkins release pipelines 2026-05-02 19:14:13 +08:00
kdletters
879a53bf8d docs: add production deployment plan 2026-05-02 02:58:14 +08:00
2272 changed files with 313978 additions and 127669 deletions

View File

@@ -0,0 +1,4 @@
Set-Location 'C:\Genarrative'
$env:RUST_SERVER_TARGET = 'http://127.0.0.1:8082'
$env:GENARRATIVE_RUNTIME_SERVER_TARGET = 'http://127.0.0.1:8082'
npm.cmd run dev:web *> 'C:\Genarrative\.codex\logs\dev-web-final.out.log'

View File

@@ -0,0 +1 @@
C:/proj/Genarrative/.hermes/skills/behavior-driven-development

View File

@@ -0,0 +1,389 @@
---
name: genarrative-play-type-integration
description: 在 Genarrative 中新增一个创作入口/玩法类型时,按入口配置、前端分流、契约、后端接口、工作台、结果页、可选 runtime 与作品架的顺序接入。
license: MIT
metadata:
author: Hermes Agent
version: "1.0"
---
# Genarrative 新增玩法类型接入流程
用于在 Genarrative 中新增一个创作入口/玩法类型,而不是单纯说明用户如何从入口创建作品。
## 适用场景
- 新增一个游戏玩法入口
- 让某个玩法从“敬请期待”变为可创建
- 为新玩法补齐创作工作台、结果页、发布与试玩链路
- 将新玩法接入创作中心作品架与广场
## 先判断接入级别
### 1. 只做入口占位
只需要新增入口配置,不接 session/workspace/result/runtime。
适合:
- 敬请期待
- 灰度占位
### 2. 可进入创作工作台
需要补齐前端分流、session、工作台、结果页至少能生成草稿。
### 3. 完整玩法闭环
需要补齐:
- 创作入口
- 工作台
- 草稿生成
- 结果页
- 发布
- 试玩 runtime
- 作品架 / 广场 / 分享
## 推荐接入顺序
### Step 1: 先定玩法 ID 和能力边界
先明确:
- `id` 是什么
- 入口是否可见
- 是否可点击创建
- 是否需要对话式创作
- 是否需要生成中页面
- 是否需要 result/runtime/gallery/share
不要先随便起临时 ID 再改名。
### Step 2: 新增入口配置
文件:
- `src/config/newWorkEntryConfig.ts`
`creationTypes` 中新增:
- `id`
- `title`
- `subtitle`
- `badge`
- `visible`
- `open`
如果只是占位:
- `visible: true`
- `open: false`
### Step 3: 确认类型过滤逻辑
文件:
- `src/components/platform-entry/platformEntryCreationTypes.ts`
检查:
- `getVisiblePlatformCreationTypes()` 是否能展示新类型
- `isPlatformCreationTypeVisible()` 是否能识别新类型
- `locked` / `hidden` 是否正确映射
### Step 4: 扩展页面阶段
文件:
- `src/components/platform-entry/platformEntryTypes.ts`
为新玩法补充 `SelectionStage`
- `*-agent-workspace`
- `*-generating`(可选)
- `*-result`
- `*-runtime`(可选)
- `*-gallery-detail`(可选)
### Step 5: 在总流程中加类型分流
文件:
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
`handleCreationHubCreateType(type)` 中新增分支,确保:
- 能进入对应工作台
- 能设置对应 `selectionStage`
- 能关闭类型弹层
同时按玩法补齐:
- `open<Play>AgentWorkspace()`
- `leave<Play>Flow()`
- `submit<Play>Message()`(对话式玩法)
- `execute<Play>Action()`
### Step 6: 接入通用 Agent flow controller
文件:
- `src/components/platform-entry/usePlatformCreationAgentFlowController.ts`
如果是 Agent 型玩法,复用通用控制器:
- `createSession`
- `getSession`
- `streamMessage`
- `executeAction`
- `isBusy`
- `error`
- `streamingReplyText`
- `selectionStage` 切换
### Step 7: 定义 shared contracts
前端:
- `packages/shared/src/contracts/`
后端:
- `server-rs/crates/shared-contracts/src/`
至少补齐:
- session snapshot
- create session request/response
- message request/response
- action request/response
- draft/result 结构
- work summary / gallery 结构(如果需要)
- runtime 结构(如果需要)
### Step 8: 实现前端 service client
目录参考:
- `src/services/`
按玩法补:
- creation client
- runtime client可选
- works client可选
- gallery client可选
建议保持和现有玩法一致的 API base 与命名风格。
### Step 9: 接后端 API
文件参考:
- `server-rs/crates/api-server/src/puzzle.rs`
- `server-rs/crates/api-server/src/puzzle_agent_turn.rs`
- `server-rs/crates/api-server/src/match3d.rs`
通常需要:
- create session
- get session
- send message
- stream message
- execute action
- publish / save / delete
- runtime start / action可选
- gallery / detail可选
后端设计优先按 Genarrative 的 DDD 分层拆开不要把玩法规则、数据库事务、LLM 调用和 HTTP handler 混在一个文件里:
- `module-<play>`纯领域规则、状态机、draft/runtime 校验,不依赖 Axum、SpacetimeDB 或外部平台。
- `shared-contracts`:前后端 DTO、请求/响应、session snapshot、draft/result/runtime 结构。
- `spacetime-module`表定义、reducer/procedure、事务编排、migration表结构变化要同步生成绑定。
- `spacetime-client`api-server 到 SpacetimeDB 的 facade隐藏 reducer 调用细节。
- `api-server`Axum 路由、鉴权、SSE/stream、应用层编排。
- `platform-*`LLM、资产上传、鉴权、第三方服务等副作用。
建议按四条线设计后端能力:
- Agent 创作线session、turn、stream、compile action。
- Works 作品线:保存、发布、删除、草稿恢复。
- Gallery 广场线公开列表、详情、like/remix/share。
- Runtime 运行态线:开始试玩、提交动作、读取状态。
### Step 10: 新增工作台组件
目录建议:
- `src/components/<play>-creation/<Play>AgentWorkspace.tsx`
两种形态:
#### 对话式
适合设定逐轮补齐。
参考:
- `BigFishAgentWorkspace.tsx`
- `Match3DAgentWorkspace.tsx`
#### 表单式
适合输入结构明确的玩法。
参考:
- `PuzzleAgentWorkspace.tsx`
### Step 11: 在渲染树中挂载新页面
文件:
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
补齐:
- workspace 分支
- generating 分支(如需要)
- result 分支
- runtime 分支(如需要)
### Step 12: 新增结果页
目录建议:
- `src/components/<play>-result/<Play>ResultView.tsx`
结果页至少支持:
- 展示 draft
- 返回编辑
- 发布
- 试玩
- 错误展示
### Step 13: 需要试玩就补 runtime
目录建议:
- `src/components/<play>-runtime/<Play>RuntimeShell.tsx`
如果玩法是游戏类,建议补完整 runtime 闭环。
### Step 14: 接入作品架 / 广场 / 分享
需要改:
- `src/components/custom-world-home/creationWorkShelf.ts`
- `src/components/custom-world-home/CustomWorldCreationHub.tsx`
- `src/services/publicWorkCode.ts`
如果玩法支持发布,还要补:
- public work code
- public detail
- publish share modal
- like/remix可选
### Step 15: 处理登录态与草稿恢复
要考虑:
- 刷新恢复草稿
- 退出登录清空私有状态
- result/draft 缺失时回退
- busy / generating / runtime 中断恢复
### Step 16: 补测试
至少覆盖:
- 入口展示
- 类型分流
- 工作台打开
- session 创建
- compile action
- result 页切换
- 发布后刷新作品架
- runtime 进入与退出
## 最小改动清单
### 只做占位
只改:
- `src/config/newWorkEntryConfig.ts`
### 做到可进入工作台
至少改:
- `newWorkEntryConfig.ts`
- `platformEntryTypes.ts`
- `PlatformEntryFlowShellImpl.tsx`
- 新玩法 service client
- 新玩法工作台组件
- shared contracts
- 后端 API
### 做到完整闭环
还要补:
- result 页
- runtime
- works / gallery
- public code
- share
- 作品架聚合
- 测试
## 常见坑
1. 只加入口配置不够,类型分流和页面阶段也要补。
2. `SelectionStage` 不扩展,前端无法安全切页。
3. 新玩法如果要出现在作品架,必须改聚合逻辑,不只是加入口。
4. 发布后不刷新 works/gallery用户会看不到新作品。
5. 如果走 SpacetimeDB表结构变化要同步 migration 和绑定;`spacetime-client/src/module_bindings/` 通常是生成物,不要为了修编译或格式化而手改,优先改 module 源 schema/reducer/procedure 后重新生成。
6. 做 analytics/tracking 这类 runtime 能力时,不要只补 API DTO先在 `module-runtime` 写纯函数测试(例如 day/week/month/quarter/year bucket 聚合、scope/event 过滤RED 后再补领域类型与聚合函数。
7. 时间粒度聚合建议复用已有 date dimension 逻辑,把 daily stat 映射到 day/week/month/quarter/year bucketbucket 输出要有稳定排序,并显式携带 `bucketKey``bucketStartDateKey``bucketEndDateKey``value`
8. 后端 shared-contracts 与前端 `packages/shared/src/contracts/runtime.ts` 要同步补 request/response/type unionadmin-web 若有独立 `api/adminApiTypes.ts`,也要同步,避免共享包已更新但管理端本地类型缺失。
9. 退出登录时要清空新玩法私有状态,避免串用户。
10. 移动端入口卡片增多后要检查布局和滚动体验。
## 验证标准
一个玩法算真正接入成功,至少要满足:
- 入口能展示
- 能进入对应工作台
- 能创建 session
- 能生成草稿
- 能进入结果页
- 能返回编辑
- 如果需要,可试玩
- 如果需要,可发布
- 发布后能回到作品架 / 广场 / 分享链路
## 建议验证命令
按改动范围选择:
```bash
# 后端 contracts / module-runtime / api-server
cd server-rs
cargo test -p shared-contracts
cargo test -p module-runtime
cargo check -p api-server
# SpacetimeDB schema/reducer/procedure 改动后,优先在有 CLI 的机器重新生成 bindings
npm run spacetime:generate -- --rust-only
# 前端类型
npm run admin-web:typecheck
```
如果新增完整前端玩法闭环,还要按项目实际脚本补充 web typecheck、lint 或 Playwright/单元测试。

View File

@@ -0,0 +1,105 @@
---
name: gpt-image-2-apimart
description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow. Use when Codex needs to create puzzle template sample images, reproduce the server-rs gpt-image-2 request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical.
---
# gpt-image-2 VectorEngine
Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine `gpt-image-2-all` path. The folder still contains `apimart` in its name for compatibility with existing local plugin references.
## Workflow
1. Read the local task and decide whether the image is project-bound.
2. Prefer `scripts/generate-template-samples.mjs` for puzzle template thumbnails or small batches.
3. Run dry-run first:
```powershell
node .codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs --dry-run
```
4. If dry-run looks correct and the user asked for real assets, run live generation with a small limit:
```powershell
node .codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs --live --limit 6
```
5. Save final project assets under `public/` or another explicitly requested workspace path.
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
## Request Contract
The repository image path uses:
```text
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
Content-Type: application/json
```
Default body:
```json
{
"model": "gpt-image-2-all",
"prompt": "<prompt>",
"n": 1,
"size": "1024x1024"
}
```
For weak visual references in text-to-image generation, add:
```json
{
"image": ["data:image/png;base64,..."]
}
```
For image-to-image work that must follow a reference image closely, use the VectorEngine edits endpoint instead of the generations `image` array:
```text
POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
Content-Type: multipart/form-data
```
Multipart fields:
```text
model=gpt-image-2
prompt=<prompt>
n=1
size=1024x1024
image=@reference.png
```
Prefer edits for workflows where the reference image controls composition, pose, container shape, or layout. In this repository, Match3D container UI generation uses edits with `public/match3d-background-references/pot-fused-reference.png` as the `image` part.
Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine GPT-image-2-all currently returns synchronously; do not poll APIMart task endpoints.
## Environment
Load environment values from process env first, then `.env.secrets.local`, `.env.local`, and `.env.example`.
Required for live generation:
- `VECTOR_ENGINE_BASE_URL`
- `VECTOR_ENGINE_API_KEY`
Optional:
- `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`
If the key or base URL is missing, stop after dry-run or explain the missing configuration. Do not ask the user to paste the key in chat.
## Prompt Rules
- Use Chinese prompts when generating project puzzle templates.
- Keep template samples square, clear, image-only, and suitable for puzzle thumbnails.
- Avoid text, watermark, UI chrome, buttons, borders, and tutorial overlays.
- Include local negative constraints in the prompt instead of relying on provider-specific negative prompt fields.
## Resources
- `scripts/generate-template-samples.mjs`: dry-run or live-generate puzzle template sample thumbnails.
- `assets/puzzle-template-prompts.json`: default prompt list consumed by the script.

View File

@@ -0,0 +1,7 @@
interface:
display_name: "GPT Image 2 VectorEngine"
short_description: "Generate project thumbnails through VectorEngine"
brand_color: "#10B981"
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through VectorEngine."
policy:
allow_implicit_invocation: true

View File

@@ -0,0 +1,62 @@
[
{
"id": "couple-memory",
"title": "情侣合照拼图",
"prompt": "温暖自然光下的一对情侣纪念合照,城市咖啡馆窗边,桌面有花束和两杯热饮,人物神情自然,画面主体清晰,前中后景层次明确,适合切成拼图。"
},
{
"id": "family-keepsake",
"title": "家庭纪念拼图",
"prompt": "三代家人在客厅沙发前的家庭纪念合照,柔和午后阳光,孩子抱着生日蛋糕,长辈微笑,画面温暖完整,细节丰富但不杂乱。"
},
{
"id": "friends-party",
"title": "朋友聚会拼图",
"prompt": "朋友们在露台夜晚聚会,彩灯、桌上零食和举杯瞬间,人物分布有层次,中央焦点清楚,氛围轻松热闹,适合社交分享拼图。"
},
{
"id": "festival-card",
"title": "节日贺卡拼图",
"prompt": "节日餐桌与礼物布置,暖色灯光、彩带、蜡烛和窗外烟花,画面像无字贺卡,主体集中,边角细节可辨,适合节日拼图。"
},
{
"id": "knowledge-summary",
"title": "知识总结拼图",
"prompt": "一张无文字的知识学习主题插画,书桌上有打开的笔记本、便签、咖啡、台灯和思维导图式图形元素,构图整洁,重点明确,适合学习打卡拼图。"
},
{
"id": "product-detail",
"title": "商品细节拼图",
"prompt": "精致商品静物展示,一只高质感香水瓶放在丝绸与花瓣之间,玻璃反光清晰,包装和材质细节丰富,背景干净,适合作为电商细节拼图。"
},
{
"id": "healing-landscape",
"title": "治愈风景拼图",
"prompt": "治愈风景插画,清晨湖边、薄雾、远山、木栈道和一盏小灯,色彩柔和,层次清楚,局部元素可辨,适合长时间拼图。"
},
{
"id": "cute-pet",
"title": "宠物可爱拼图",
"prompt": "一只可爱的橘猫趴在阳光窗台上,旁边有绿植、毛线球和小毯子,猫的表情清楚,画面温柔干净,适合萌宠拼图分享。"
},
{
"id": "hot-topic-poster",
"title": "热点海报拼图",
"prompt": "电影感热点海报风插画,雨夜街头、霓虹反光、奔跑的人影和远处光束,强烈视觉焦点,画面无文字,适合热点话题拼图。"
},
{
"id": "event-invitation",
"title": "活动邀请拼图",
"prompt": "活动邀请主题插画,展厅入口、花艺装置、签到台和柔和灯带,人群剪影自然分布,画面高级干净,无文字,适合活动预热拼图。"
},
{
"id": "daily-challenge",
"title": "每日挑战拼图",
"prompt": "每日挑战主题插画,清爽桌面上摆放相机、明信片、计时器和小奖章,色彩明亮,构图有趣,细节可拆解,适合平台每日拼图。"
},
{
"id": "children-learning",
"title": "儿童认知拼图",
"prompt": "儿童认知学习插画,木质桌面上有积木、彩色形状、动物玩偶和小书本,色彩明快,元素边界清晰,无文字,适合儿童教育拼图。"
}
]

View File

@@ -0,0 +1,351 @@
import { Buffer } from 'node:buffer';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations');
const defaultTimeoutMs = 1000000;
const prompts = [
{
id: 'cat-barista',
title: '咖啡师猫咪',
subject:
'一只奶油色猫咪像人一样双足站立,穿深绿色围裙,在温暖咖啡馆吧台前专注拉花,爪子扶着咖啡杯,蓬松尾巴自然弯起,童书级精致插画,柔和自然光,主体清晰。',
},
{
id: 'cat-detective',
title: '侦探猫咪',
subject:
'一只黑白猫咪像侦探一样双足站在雨后街角,穿短风衣和小帽子,单爪拿放大镜,另一只爪插兜,路灯和湿润石板路反光,电影感但可爱,插画风格。',
},
{
id: 'cat-dancer',
title: '舞者猫咪',
subject:
'一只橘猫以拟人舞者姿态单脚旋转,穿轻盈舞台披肩,前爪展开,尾巴形成优雅弧线,背景是暖色小剧场灯光,动作灵动,精致插画。',
},
{
id: 'cat-knight',
title: '骑士猫咪',
subject:
'一只银灰猫咪像小骑士一样站在苔藓石台上,披短斗篷,双爪握着细剑指向地面,姿态勇敢但可亲,远处森林微光,奇幻插画风格。',
},
{
id: 'cat-painter',
title: '画家猫咪',
subject:
'一只三花猫咪双足站在画架前,穿宽松蓝色工作衫,一爪拿画笔一爪托调色盘,鼻尖有颜料点,窗边画室阳光明亮,温柔手绘插画。',
},
{
id: 'cat-astronaut',
title: '宇航员猫咪',
subject:
'一只白猫咪以拟人宇航员姿态站在月面,透明头盔内露出猫脸,尾巴在宇航服后轻轻翘起,爪子向远处蓝色星球敬礼,梦幻插画风格。',
},
];
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
const raw = process.argv[index];
if (raw.startsWith('--')) {
const next = process.argv[index + 1];
if (next && !next.startsWith('--')) {
args.set(raw, next);
index += 1;
} else {
args.set(raw, true);
}
}
}
function readDotenv(fileName) {
const filePath = path.join(repoRoot, fileName);
if (!existsSync(filePath)) {
return {};
}
const values = {};
for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/u)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed);
if (!match) {
continue;
}
let value = match[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
values[match[1]] = value;
}
return values;
}
function resolveEnv() {
const loaded = {
...readDotenv('.env.example'),
...readDotenv('.env.local'),
...readDotenv('.env.secrets.local'),
...process.env,
};
return {
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildVectorEngineImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
}
function buildPrompt(entry) {
return [
'请生成一张高清 1:1 方形插画。',
`画面主体:${entry.subject}`,
'要求:猫咪保留清晰猫脸、猫耳、猫尾和毛发质感,但身体姿态像人一样自然;构图完整,角色占画面主体,适合作为项目插画素材。',
'避免文字、水印、边框、按钮、UI 元素、低清晰度、过度写实恐怖感、畸形肢体、多余手指。',
].join('');
}
function collectStringsByKey(value, targetKey, output) {
if (Array.isArray(value)) {
value.forEach((entry) => collectStringsByKey(entry, targetKey, output));
return;
}
if (!value || typeof value !== 'object') {
return;
}
for (const [key, nested] of Object.entries(value)) {
if (key === targetKey) {
if (typeof nested === 'string' && nested.trim()) {
output.push(nested.trim());
}
if (Array.isArray(nested)) {
nested.forEach((entry) => {
if (typeof entry === 'string' && entry.trim()) {
output.push(entry.trim());
}
});
}
}
collectStringsByKey(nested, targetKey, output);
}
}
function extractImageUrls(payload) {
const urls = [];
collectStringsByKey(payload, 'url', urls);
collectStringsByKey(payload, 'image', urls);
collectStringsByKey(payload, 'image_url', urls);
return [...new Set(urls)].filter((url) => /^https?:\/\//u.test(url));
}
function extractBase64Images(payload) {
const values = [];
collectStringsByKey(payload, 'b64_json', values);
return values;
}
function inferExtensionFromContentType(contentType) {
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
if (normalized === 'image/png') {
return 'png';
}
if (normalized === 'image/webp') {
return 'webp';
}
if (normalized === 'image/gif') {
return 'gif';
}
return 'jpg';
}
function inferExtensionFromBytes(bytes) {
if (bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1A\n', 'binary'))) {
return 'png';
}
if (bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) {
return 'jpg';
}
if (
bytes.subarray(0, 4).toString('ascii') === 'RIFF' &&
bytes.subarray(8, 12).toString('ascii') === 'WEBP'
) {
return 'webp';
}
return 'png';
}
async function fetchJson(url, options, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: abortController.signal,
});
const text = await response.text();
if (!response.ok) {
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
}
return JSON.parse(text);
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function downloadUrl(url, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: abortController.signal });
if (!response.ok) {
throw new Error(`download ${response.status}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
return {
bytes,
extension: inferExtensionFromContentType(
response.headers.get('content-type') || 'image/jpeg',
),
};
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`Generated image download timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function generateOne(env, entry, outDir) {
const requestBody = {
model: 'gpt-image-2-all',
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
};
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const urls = extractImageUrls(payload);
const b64Images = extractBase64Images(payload);
let image;
if (urls[0]) {
image = await downloadUrl(urls[0], env.timeoutMs);
} else if (b64Images[0]) {
const bytes = Buffer.from(b64Images[0], 'base64');
image = {
bytes,
extension: inferExtensionFromBytes(bytes),
};
} else {
throw new Error(`VectorEngine returned no image for ${entry.id}`);
}
mkdirSync(outDir, { recursive: true });
const outputPath = path.join(outDir, `${entry.id}.${image.extension}`);
writeFileSync(outputPath, image.bytes);
return outputPath;
}
const dryRun = args.has('--dry-run') || !args.has('--live');
const outDir = path.resolve(String(args.get('--out-dir') || defaultOutDir));
const limit = Number.parseInt(String(args.get('--limit') || '0'), 10);
const selectedPrompts = limit > 0 ? prompts.slice(0, limit) : prompts;
if (dryRun) {
const env = resolveEnv();
console.log(
JSON.stringify(
{
mode: 'dry-run',
outDir,
count: selectedPrompts.length,
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
requests: selectedPrompts.map((entry) => ({
id: entry.id,
title: entry.title,
body: {
model: 'gpt-image-2-all',
prompt: buildPrompt(entry),
n: 1,
size: '1024x1024',
},
})),
},
null,
2,
),
);
process.exit(0);
}
const env = resolveEnv();
if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),
);
process.exit(1);
}
const generated = [];
for (const entry of selectedPrompts) {
console.log(`Generating ${entry.id}...`);
generated.push(await generateOne(env, entry, outDir));
}
console.log(
JSON.stringify(
{
ok: true,
count: generated.length,
files: generated,
},
null,
2,
),
);

View File

@@ -0,0 +1,321 @@
import { Buffer } from 'node:buffer';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const skillRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(skillRoot, '..', '..', '..');
const promptsPath = path.join(
skillRoot,
'assets',
'puzzle-template-prompts.json',
);
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
const defaultTimeoutMs = 1000000;
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
const raw = process.argv[index];
if (raw.startsWith('--')) {
const next = process.argv[index + 1];
if (next && !next.startsWith('--')) {
args.set(raw, next);
index += 1;
} else {
args.set(raw, true);
}
}
}
function readDotenv(fileName) {
const filePath = path.join(repoRoot, fileName);
if (!existsSync(filePath)) {
return {};
}
const values = {};
for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/u)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u.exec(trimmed);
if (!match) {
continue;
}
let value = match[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
values[match[1]] = value;
}
return values;
}
function resolveEnv() {
const loaded = {
...readDotenv('.env.example'),
...readDotenv('.env.local'),
...readDotenv('.env.secrets.local'),
...process.env,
};
return {
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
.trim()
.replace(/\/+$/u, ''),
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
timeoutMs: Number.parseInt(
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
10,
),
};
}
function buildVectorEngineImagesGenerationUrl(baseUrl) {
return baseUrl.endsWith('/v1')
? `${baseUrl}/images/generations`
: `${baseUrl}/v1/images/generations`;
}
function buildPrompt(template) {
return [
'请生成一张高清 1:1 方形插画,用作拼图创作模板样例图。',
`画面主体:${template.prompt}`,
'要求:主体清晰集中,前中后景层次明确,边角有可辨识细节,适合切成 3x3 到 7x7 拼图。',
'避免文字、水印、边框、按钮、UI 元素、教程标注、低清晰度、过度模糊、杂乱构图。',
].join('');
}
function collectStringsByKey(value, targetKey, output) {
if (Array.isArray(value)) {
value.forEach((entry) => collectStringsByKey(entry, targetKey, output));
return;
}
if (!value || typeof value !== 'object') {
return;
}
for (const [key, nested] of Object.entries(value)) {
if (key === targetKey) {
if (typeof nested === 'string' && nested.trim()) {
output.push(nested.trim());
}
if (Array.isArray(nested)) {
nested.forEach((entry) => {
if (typeof entry === 'string' && entry.trim()) {
output.push(entry.trim());
}
});
}
}
collectStringsByKey(nested, targetKey, output);
}
}
function extractImageUrls(payload) {
const urls = [];
collectStringsByKey(payload, 'url', urls);
collectStringsByKey(payload, 'image', urls);
collectStringsByKey(payload, 'image_url', urls);
return [...new Set(urls)].filter((url) => /^https?:\/\//u.test(url));
}
function extractBase64Images(payload) {
const values = [];
collectStringsByKey(payload, 'b64_json', values);
return values;
}
function inferExtensionFromContentType(contentType) {
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
if (normalized === 'image/png') {
return 'png';
}
if (normalized === 'image/webp') {
return 'webp';
}
if (normalized === 'image/gif') {
return 'gif';
}
return 'jpg';
}
function inferExtensionFromBytes(bytes) {
if (bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1A\n', 'binary'))) {
return 'png';
}
if (bytes.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) {
return 'jpg';
}
if (
bytes.subarray(0, 4).toString('ascii') === 'RIFF' &&
bytes.subarray(8, 12).toString('ascii') === 'WEBP'
) {
return 'webp';
}
return 'png';
}
async function fetchJson(url, options, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: abortController.signal,
});
const text = await response.text();
if (!response.ok) {
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
}
return JSON.parse(text);
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function downloadUrl(url, timeoutMs) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: abortController.signal });
if (!response.ok) {
throw new Error(`download ${response.status}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
return {
bytes,
extension: inferExtensionFromContentType(
response.headers.get('content-type') || 'image/jpeg',
),
};
} catch (error) {
if (error?.name === 'AbortError') {
throw new Error(`Generated image download timed out after ${timeoutMs}ms`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function generateOne(env, template, outDir) {
const requestBody = {
model: 'gpt-image-2-all',
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
};
const payload = await fetchJson(
buildVectorEngineImagesGenerationUrl(env.baseUrl),
{
method: 'POST',
headers: {
Authorization: `Bearer ${env.apiKey}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
},
env.timeoutMs,
);
const urls = extractImageUrls(payload);
const b64Images = extractBase64Images(payload);
let image;
if (urls[0]) {
image = await downloadUrl(urls[0], env.timeoutMs);
} else if (b64Images[0]) {
const bytes = Buffer.from(b64Images[0], 'base64');
image = {
bytes,
extension: inferExtensionFromBytes(bytes),
};
} else {
throw new Error(`VectorEngine returned no image for ${template.id}`);
}
mkdirSync(outDir, { recursive: true });
const outputPath = path.join(outDir, `${template.id}.${image.extension}`);
writeFileSync(outputPath, image.bytes);
return outputPath;
}
const dryRun = args.has('--dry-run') || !args.has('--live');
const outDir = path.resolve(String(args.get('--out-dir') || defaultOutDir));
const limit = Number.parseInt(String(args.get('--limit') || '0'), 10);
const onlyIds = String(args.get('--only') || '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const templates = JSON.parse(readFileSync(promptsPath, 'utf8')).filter(
(template) => !onlyIds.length || onlyIds.includes(template.id),
);
const selectedTemplates = limit > 0 ? templates.slice(0, limit) : templates;
if (dryRun) {
console.log(
JSON.stringify(
{
mode: 'dry-run',
outDir,
count: selectedTemplates.length,
requests: selectedTemplates.map((template) => ({
id: template.id,
title: template.title,
body: {
model: 'gpt-image-2-all',
prompt: buildPrompt(template),
n: 1,
size: '1024x1024',
},
})),
},
null,
2,
),
);
process.exit(0);
}
const env = resolveEnv();
if (!env.baseUrl || !env.apiKey) {
console.error(
JSON.stringify({
ok: false,
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
hasBaseUrl: Boolean(env.baseUrl),
hasApiKey: Boolean(env.apiKey),
}),
);
process.exit(1);
}
const generated = [];
for (const template of selectedTemplates) {
console.log(`Generating ${template.id}...`);
generated.push(await generateOne(env, template, outDir));
}
console.log(
JSON.stringify(
{
ok: true,
count: generated.length,
files: generated,
},
null,
2,
),
);

View File

@@ -0,0 +1,121 @@
---
name: wechatpay-basic-payment
description: 微信支付基础支付解决方案,涵盖支付、退款账单、分账、商户进件、开户意愿确认,提供选型/代码示例/业务速查/质量评估/排障五大能力。Use when user mentions "JSAPI支付", "APP支付", "H5支付", "Native支付", "小程序支付", "付款码支付", "合单支付", "特约商户进件", "开户意愿确认", or asks to "推荐支付方式", "要支付接口代码示例", "排查支付或退款问题".
author: wechatpay
version: "1.0"
---
# 微信支付基础支付 & 合单支付接入指引
## 全局交互规范
> ‼️ 以下规则适用于本技能所有能力、所有对话轮次,优先级高于各能力的局部规则。
1. **所有问题必须得到用户明确回答后才能继续。** 如果一次提出了多个问题,必须逐一检查每个问题是否都已获得用户的明确答复。对于未回答的问题,必须再次追问,**严禁对未回答的问题自行假设、推断或使用默认值**。
2. **接入模式前置确认**:任何能力使用前须先确认**商户模式**或**服务商模式**,已明确则无需重复。两种模式的核心差异见 → [📄 接入模式说明.md](./references/3-商户与服务商通用/接入指南/接入模式说明.md)。
3. **分步确认协议**(简单知识问答除外,需要帮用户排查、分析或执行操作时必须遵守):
- **① 明确需求**:先理解用户问题,给出初步判断或原因分析,不要一上来就堆参数清单。
- **② 征得同意**:主动提出下一步能做什么,**等用户明确同意后**才继续,严禁用户没表态就开始收集参数或执行操作。
- **③ 收集信息**:用户同意后再告知需要哪些信息并逐项收集,收齐才能执行。
- **④ 执行前确认**:准备执行操作前,简要说明即将做什么,确认用户同意后再执行;涉及线上环境须额外提示风险。
## 能力概览
1. **产品选型** — 根据场景推荐支付方式JSAPI/APP/H5/Native/小程序/付款码),判断是否需要合单支付
2. **示例代码** — 各接口的下单、调起、回调、退款、账单等代码结构示例(只展示不写入)
3. **业务知识速查** — 订单状态、退款规则、账单对账、APPID绑定、特约商户进件、开户意愿确认等
4. **接入质量评估** — 签名验签、业务逻辑完整性、回调处理规范性检查(含合单/分账/进件/开户意愿确认专项)
5. **问题排查** — 下单失败、调起异常、回调收不到、退款失败等(含合单支付专项常见问题)
> 未明确支付方式时先通过能力1引导选型。退款和账单无需确认支付方式但仍需确认接入模式。合单支付需先确认是否涉及多商户/多APPID场景。特约商户进件和商户开户意愿确认仅适用于服务商/渠道商模式。
## 能力1产品选型
> 用户问「该用哪种支付方式」或比较各方式区别时 → 加载 `支付产品对比.md`,确定支付方式后再按需加载示例代码。
- 产品对比 + 选型决策树 + 准入条件 + 调起支付差异 → [📄 支付产品对比.md](./references/3-商户与服务商通用/产品选型/支付产品对比.md)
## 能力2示例代码
> 用户要某个接口的代码示例时 → 确认接入模式和语言,加载对应模式的 `接口索引.md` 定位代码文件。
>
> ‼️ **只检索、不生成。** 严禁从零编写任何代码,必须从代码示例文件中检索获取。
>
> ‼️ **只展示、不写入。** 代码示例仅用于讲解 API 调用结构和签名流程,严禁直接写入用户项目(禁止调用 write_to_file、replace_in_file 等工具创建或修改项目文件)。在对话中展示代码,让用户自行复制适配。
>
> ‼️ **先交互、后输出。** 提供代码前必须先确认接入模式、开发语言和具体接口,每次只输出一个接口;提供完代码后主动推荐接入质量评估。
>
> ‼️ **支付方式仅「下单」和「调起支付」接口需确认,其他接口无需询问支付方式。** 用户请求查单、关单、退款、回调处理、账单等通用接口时,只需确认接入模式和开发语言,无需询问支付方式——这些接口各支付方式完全相同。**但合单支付的查单、关单、回调使用专用接口,需确认用户是基础支付还是合单支付。**
>
> ‼️ **用户语言非 Java/Go 时**(本 skill 仅维护 Java/Go 示例):**禁止**直接生成跨语言代码。流程:
> 1. 用 `AskQuestion` 获明确同意(文案需明示「参考实现 / 非官方维护 / 须自行 review 与测试」),未同意只发官方 Java/Go 原文。
> 2. 同意后以官方 Java 示例为基准翻译生成业务代码「参考实现」;再用纯文字问是否翻 Java 公库SDK 工具类 + HTTP 客户端),未明确要不贴。每段代码前附下方免责块。
>
> > ⚠️ 以下代码为**跨语言参考实现**,由 AI 参考官方 Java 示例翻译生成,并非微信支付官方维护。
> > - 请**逐行 review** 签名构造、HTTP 调用、字段命名、回调解密等关键逻辑。
> > - 上线前必须在测试环境完整验证,建议先以官方 Java/Go 示例打通主链路作为对照。
> > - 出现接入问题时以官方 Java/Go 示例为准。
- 涉及提供示例代码时,按接入模式查阅对应接口索引,定位目标代码文件:
- 商户模式 → [📄 接口索引.md](./references/1-商户/示例代码/接口索引.md)
- 服务商模式 → [📄 接口索引.md](./references/2-服务商/示例代码/接口索引.md)
> **加载策略**:先确认接入模式,读对应的 `接口索引.md` 定位用户需要的接口对应的文件路径,再按需加载具体文件。不要一次性加载所有文件。
## 能力3业务知识速查
> 用户问参数获取、APPID绑定、订单状态、退款规则、分账等业务知识时 → 按接入模式加载对应文档。
- 开发必要参数 / APPID类型 / APPID绑定流程
- 商户模式 → [📄 开发必要参数说明.md](./references/1-商户/接入指南/开发必要参数说明.md)
- 服务商模式 → [📄 开发必要参数说明.md](./references/2-服务商/接入指南/开发必要参数说明.md)
- 点金计划(服务商 JSAPI 必接) → [📄 点金计划.md](./references/2-服务商/接入指南/点金计划.md)
- 订单状态 / 关单 / 终态 → [📄 订单状态流转.md](./references/3-商户与服务商通用/接入指南/订单状态流转.md)
- 分账 → [📄 分账接入指南.md](./references/3-商户与服务商通用/接入指南/分账接入指南.md)
- 特约商户进件(仅服务商) → [📄 特约商户进件.md](./references/2-服务商/接入指南/特约商户进件.md)
- 商户开户意愿确认(仅服务商/渠道商) → [📄 商户开户意愿确认.md](./references/2-服务商/接入指南/商户开户意愿确认.md)
- 退款规则 / 账单对账 → 已整合到示例代码注释中通过能力2加载
> **加载策略**:按关键词匹配文档,区分接入模式。特约商户进件和商户开户意愿确认为服务商/渠道商专属,商户模式无需加载。
## 能力4接入质量评估
> 用户准备上线或想检查代码隐患时 → 加载以下文档。
>
> ‼️ **只检查用户实际使用的功能模块。** 合单支付、分账、进件、开户意愿确认等模块须先确认用户是否涉及,**未使用的不检查、不提及**。
- 签名验签 → [📄 签名与验签规则.md](./references/3-商户与服务商通用/接入指南/签名与验签规则.md)
- 业务逻辑完整性(含质检人设 + 检查清单) → [📄 接入质量检查清单.md](./references/3-商户与服务商通用/接入指南/接入质量检查清单.md)
- 回调处理规范 → [📄 回调通知处理.md](./references/3-商户与服务商通用/接入指南/回调通知处理.md)
## 能力5问题排查
> 用户遇到报错或接口调用异常时 → 按下方路径分流加载。
>
> ‼️ **排障推荐示例代码时,必须先确认开发语言,只推荐对应的示例。** 排障手册中每个错误码的「示例代码推荐」可能涉及 Java/Go 两种语言示例,但输出时**只输出匹配的示例**。开发语言尚未确认时,先在推荐示例代码时自然地询问用户。
>
> ‼️ **用户语言非 Java/Go 时按能力 2 的跨语言确认流程处理**(弹框确认 → 参考生成 + 免责块 + 公库分步)。先用文字说明 Java/Go 示例中的关键修复点(签名、字段、流程),再走完整流程后再生成对应语言的"参考修复代码"。
- 排障手册(错误码 TOP 20 速查 + 定位流程 + 服务商特有问题)→ [📄 排障手册.md](./references/3-商户与服务商通用/问题排查/排障手册.md)
- 基础支付常见问题 → [📄 基础支付常见问题.md](./references/3-商户与服务商通用/问题排查/基础支付常见问题.md)
- 分账常见问题 → [📄 分账常见问题.md](./references/3-商户与服务商通用/问题排查/分账常见问题.md)
- 合单支付常见问题 → [📄 合单支付常见问题.md](./references/3-商户与服务商通用/问题排查/合单支付常见问题.md)
- 排障辅助脚本(排障手册中 🔧 标注的场景):`scripts/商户/``scripts/服务商/` 下各有 `查询订单.py``查询退款.py`
> **加载策略**
>
> - **路径A有 Request-Id**→ 读 `排障手册.md`,提取错误码匹配 TOP 20 速查表直接给出方案;标注 🔧 的引导用户执行脚本。未命中则按手册各章节排查,仍未解决再加载对应常见问题文档兜底。
> - **路径B无 Request-Id**→ 确认支付方式,加载对应常见问题文档匹配。未命中再加载 `排障手册.md` 兜底。
> - **路径C进件/开户意愿确认)**→ 直接加载 `特约商户进件.md` 或 `商户开户意愿确认.md`,文档末尾的常见问题和常见报错覆盖高频问题。
>
> **脚本使用规范**脚本采用签名模式不获取用户私钥。引导用户在自己服务器完成签名后将签名值Base64、时间戳、随机串传入脚本。执行前需按分步确认协议征得同意。
---
> 以下信息与技能能力无关,仅供查阅。
## 💬 社区与反馈
在使用过程中遇到问题、有改进建议,或者想和其他开发者交流接入经验,欢迎扫码添加企业微信进群,与官方团队和社区开发者一起讨论:
![微信支付 Skills 交流群二维码](https://raw.githubusercontent.com/wechatpay-apiv3/wechatpay-skills/main/wechatpay-basic-payment/assets/qrcode.jpg)

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -0,0 +1,83 @@
# 开发必要参数说明
普通商户模式接入微信支付 APIv3 前需要准备开发必要参数mchid、appid、商户API证书、微信支付公钥、APIv3密钥等
## APPID 详解与绑定
### APPID 类型
APPID 是微信生态中应用的唯一标识,格式都是 `wx` + 一串字符(如 `wxd678efh567hg6787`),根据注册平台不同分为三种类型:
| APPID 类型 | 注册平台 | 用途 |
|-----------|---------|------|
| 公众号 AppID | 公众平台mp.weixin.qq.com | 服务号/订阅号,用于公众号内网页场景 |
| 小程序 AppID | 公众平台mp.weixin.qq.com | 微信小程序场景 |
| 移动应用 AppID | 开放平台open.weixin.qq.com | 原生 APPiOS/Android/鸿蒙)场景 |
> **三种 APPID 格式相同但不能混用**。拿小程序 AppID 做 JSAPI 支付会报错,拿公众号 AppID 做 APP 支付也不行。
### 为什么需要绑定 APPID
微信支付的所有支付方式都要求商户号与 APPID 建立绑定关系,未绑定时下单接口会报错。
### 如何查询 APPID
| APPID 类型 | 查询路径 |
|-----------|---------|
| 服务号/公众号 | 登录公众平台 → 设置与开发 → 开发接口管理 → 基本配置 → 开发者ID(AppID) |
| 小程序 | 登录公众平台 → 开发与服务 → 开发管理 → 开发设置 → AppID(小程序ID) |
| 移动应用 | 登录开放平台 → 管理中心 → 移动应用 → 查看 → 详情页面 → APPID |
### 如何绑定
**第一步:在商户平台发起绑定申请**
登录商户平台 → 产品中心 → APPID授权管理 → +关联AppID → 新增授权 → 填写 APPID → 提交
- 主体一致:直接填写 APPID 提交
- 主体不一致:还需填写 APPID 认证主体,并勾选《微信支付联合营运承诺函》
**第二步:在对应平台确认授权**
| APPID 类型 | 确认路径 |
|-----------|---------|
| 服务号/公众号 | 登录公众平台 → 微信支付 → 商户号管理 → 待关联商户号 → 确认 |
| 小程序 | 登录公众平台 → 微信支付 → 商户号管理 → 待关联商户号 → 确认 |
| 移动应用 | 登录开放平台 → 移动应用 → 详情 → 能力专区 → 微信支付 → 查询详情 → 待关联商户号 → 确认 |
> 委托第三方创建的小程序,需先设置邮箱密码后登录 PC 端确认。
**第三步:查看绑定结果**
登录商户平台 → 产品中心 → APPID账号管理 → 我关联的APPID账号
### 绑定限制
| 限制项 | 说明 |
|-------|------|
| 数量上限 | 一个商户号最多关联 50 个 APPID |
| 解绑 | 绑定后不支持解绑,每条关系相互独立 |
| 跨主体 | 需补充 APPID 主体信息 |
| 特殊费率 | 享有特殊行业费率的商户号提交后有额外审核1-3个工作日 |
| 费率一致性 | APPID 已绑定其他商户号时,新商户号的费率需与已绑定的一致 |
| 风控 | 商户号或 APPID 存在风险时(资料不全、有未处理处罚等),可能增加审核或被驳回 |
### APPID 相关常见报错
| 报错信息 | 原因 | 处理方式 |
|---------|------|---------|
| `appid and mchid not match` | 下单时传入的 appid 与商户号未建立绑定关系 | 按上述流程绑定 |
| `appid is invalid` | appid 格式不对,或使用了错误类型的 appid | 检查是否用了正确类型的 APPID如 JSAPI 需要公众号 AppID不能用小程序 AppID |
| JSAPI 支付报权限错误 | 商户号绑定的是小程序 APPID但用 JSAPI 调起 | JSAPI 需要绑定服务号 APPID |
## 参数与代码示例的对应关系
示例代码中构造函数所需的参数与上述开发必要参数的对应:
```
mchid → 商户号
certificateSerialNo → 商户API证书序列号
privateKeyFilePath → 商户API证书私钥文件路径apiclient_key.pem
wechatPayPublicKeyId → 微信支付公钥ID
wechatPayPublicKeyFilePath → 微信支付公钥文件路径wxp_pub.pem
```

View File

@@ -0,0 +1,203 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &DirectApiv3JsapiPrepayRequest{
Appid: wxpay_utility.String("wxd678efh567hg6787"),
Mchid: wxpay_utility.String("1230000109"),
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
TimeExpire: wxpay_utility.Time(time.Now()),
Attach: wxpay_utility.String("自定义数据说明"),
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
GoodsTag: wxpay_utility.String("WXG"),
SupportFapiao: wxpay_utility.Bool(false),
Amount: &CommonAmountInfo{
Total: wxpay_utility.Int64(100),
Currency: wxpay_utility.String("CNY"),
},
Payer: &JsapiReqPayerInfo{
Openid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
},
Detail: &CouponInfo{
CostPrice: wxpay_utility.Int64(608800),
InvoiceId: wxpay_utility.String("微信123"),
GoodsDetail: []GoodsDetail{GoodsDetail{
MerchantGoodsId: wxpay_utility.String("1246464644"),
WechatpayGoodsId: wxpay_utility.String("1001"),
GoodsName: wxpay_utility.String("iPhoneX 256G"),
Quantity: wxpay_utility.Int64(1),
UnitPrice: wxpay_utility.Int64(528800),
}},
},
SceneInfo: &CommonSceneInfo{
PayerClientIp: wxpay_utility.String("14.23.150.211"),
DeviceId: wxpay_utility.String("013467007045764"),
StoreInfo: &StoreInfo{
Id: wxpay_utility.String("0001"),
Name: wxpay_utility.String("腾讯大厦分店"),
AreaCode: wxpay_utility.String("440305"),
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
},
},
SettleInfo: &SettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
}
response, err := JsapiPrepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func JsapiPrepay(config *wxpay_utility.MchConfig, request *DirectApiv3JsapiPrepayRequest) (response *DirectApiv3JsapiPrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/pay/transactions/jsapi"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &DirectApiv3JsapiPrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type DirectApiv3JsapiPrepayRequest struct {
Appid *string `json:"appid,omitempty"`
Mchid *string `json:"mchid,omitempty"`
Description *string `json:"description,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
Attach *string `json:"attach,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SupportFapiao *bool `json:"support_fapiao,omitempty"`
Amount *CommonAmountInfo `json:"amount,omitempty"`
Payer *JsapiReqPayerInfo `json:"payer,omitempty"`
Detail *CouponInfo `json:"detail,omitempty"`
SceneInfo *CommonSceneInfo `json:"scene_info,omitempty"`
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
}
type DirectApiv3JsapiPrepayResponse struct {
PrepayId *string `json:"prepay_id,omitempty"`
}
type CommonAmountInfo struct {
Total *int64 `json:"total,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type JsapiReqPayerInfo struct {
Openid *string `json:"openid,omitempty"`
}
type CouponInfo struct {
CostPrice *int64 `json:"cost_price,omitempty"`
InvoiceId *string `json:"invoice_id,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type CommonSceneInfo struct {
PayerClientIp *string `json:"payer_client_ip,omitempty"`
DeviceId *string `json:"device_id,omitempty"`
StoreInfo *StoreInfo `json:"store_info,omitempty"`
}
type SettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
}
type StoreInfo struct {
Id *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
AreaCode *string `json:"area_code,omitempty"`
Address *string `json:"address,omitempty"`
}

View File

@@ -0,0 +1,177 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
)
// 申请交易账单API
//
// 关键注意:
// 1. 次日10点后拉取API仅支持3个月内单日账单更早的需在商户平台下载。
// 2. 返回的是下载链接download_url需二次请求下载gzip压缩CSV
// 3. 账单金额单位为"元"与下单API的"分"不同,对账时注意转换。
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &GetTradeBillRequest{
BillDate: wxpay_utility.String("2019-06-11"),
BillType: BILLTYPE_ALL.Ptr(),
TarType: TARTYPE_GZIP.Ptr(),
}
response, err := GetTradeBill(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// GetTradeBill 申请交易账单API
func GetTradeBill(config *wxpay_utility.MchConfig, request *GetTradeBillRequest) (response *QueryBillEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/bill/tradebill"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
query := reqUrl.Query()
if request.BillDate != nil {
query.Add("bill_date", *request.BillDate)
}
if request.BillType != nil {
query.Add("bill_type", fmt.Sprintf("%v", *request.BillType))
}
if request.TarType != nil {
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &QueryBillEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type GetTradeBillRequest struct {
BillDate *string `json:"bill_date,omitempty"`
BillType *BillType `json:"bill_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
}
func (o *GetTradeBillRequest) MarshalJSON() ([]byte, error) {
type Alias GetTradeBillRequest
a := &struct {
BillDate *string `json:"bill_date,omitempty"`
BillType *BillType `json:"bill_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
BillDate: nil,
BillType: nil,
TarType: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type QueryBillEntity struct {
HashType *HashType `json:"hash_type,omitempty"`
HashValue *string `json:"hash_value,omitempty"`
DownloadUrl *string `json:"download_url,omitempty"`
}
type BillType string
func (e BillType) Ptr() *BillType {
return &e
}
const (
BILLTYPE_ALL BillType = "ALL"
BILLTYPE_SUCCESS BillType = "SUCCESS"
BILLTYPE_REFUND BillType = "REFUND"
)
type TarType string
func (e TarType) Ptr() *TarType {
return &e
}
const (
TARTYPE_GZIP TarType = "GZIP"
)
type HashType string
func (e HashType) Ptr() *HashType {
return &e
}
const (
HASHTYPE_SHA1 HashType = "SHA1"
)

View File

@@ -0,0 +1,171 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &GetFundFlowBillRequest{
BillDate: wxpay_utility.String("2019-06-11"),
AccountType: FUNDFLOWBILLACCOUNTTYPE_BASIC.Ptr(),
TarType: TARTYPE_GZIP.Ptr(),
}
response, err := GetFundFlowBill(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// GetFundFlowBill 申请资金账单API
func GetFundFlowBill(config *wxpay_utility.MchConfig, request *GetFundFlowBillRequest) (response *QueryBillEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/bill/fundflowbill"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
query := reqUrl.Query()
if request.BillDate != nil {
query.Add("bill_date", *request.BillDate)
}
if request.AccountType != nil {
query.Add("account_type", fmt.Sprintf("%v", *request.AccountType))
}
if request.TarType != nil {
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &QueryBillEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type GetFundFlowBillRequest struct {
BillDate *string `json:"bill_date,omitempty"`
AccountType *FundFlowBillAccountType `json:"account_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
}
func (o *GetFundFlowBillRequest) MarshalJSON() ([]byte, error) {
type Alias GetFundFlowBillRequest
a := &struct {
BillDate *string `json:"bill_date,omitempty"`
AccountType *FundFlowBillAccountType `json:"account_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
BillDate: nil,
AccountType: nil,
TarType: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type QueryBillEntity struct {
HashType *HashType `json:"hash_type,omitempty"`
HashValue *string `json:"hash_value,omitempty"`
DownloadUrl *string `json:"download_url,omitempty"`
}
type FundFlowBillAccountType string
func (e FundFlowBillAccountType) Ptr() *FundFlowBillAccountType {
return &e
}
const (
FUNDFLOWBILLACCOUNTTYPE_BASIC FundFlowBillAccountType = "BASIC"
FUNDFLOWBILLACCOUNTTYPE_OPERATION FundFlowBillAccountType = "OPERATION"
FUNDFLOWBILLACCOUNTTYPE_FEES FundFlowBillAccountType = "FEES"
)
type TarType string
func (e TarType) Ptr() *TarType {
return &e
}
const (
TARTYPE_GZIP TarType = "GZIP"
)
type HashType string
func (e HashType) Ptr() *HashType {
return &e
}
const (
HASHTYPE_SHA1 HashType = "SHA1"
)

View File

@@ -0,0 +1,71 @@
package wxpay_utility
import (
"bytes"
"io"
"net/http"
)
const Host = "https://api.mch.weixin.qq.com"
// SendGet 发送 GET 请求并返回已验签的应答 Body
func SendGet(config *MchConfig, uri string) ([]byte, error) {
return sendRequest(config, "GET", uri, nil)
}
// SendPost 发送 POST 请求并返回已验签的应答 Body
func SendPost(config *MchConfig, uri string, reqBody []byte) ([]byte, error) {
return sendRequest(config, "POST", uri, reqBody)
}
func sendRequest(config *MchConfig, method string, uri string, reqBody []byte) ([]byte, error) {
var bodyReader io.Reader
if reqBody != nil {
bodyReader = bytes.NewReader(reqBody)
}
httpRequest, err := http.NewRequest(method, Host+uri, bodyReader)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := BuildAuthorization(config.MchId(), config.CertificateSerialNo(),
config.PrivateKey(), method, uri, reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
if reqBody != nil {
httpRequest.Header.Set("Content-Type", "application/json")
}
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
return respBody, nil
}
return nil, NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}

View File

@@ -0,0 +1,539 @@
package wxpay_utility
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"hash"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/tjfoc/gmsm/sm3"
)
type MchConfig struct {
mchId string
certificateSerialNo string
privateKeyFilePath string
wechatPayPublicKeyId string
wechatPayPublicKeyFilePath string
privateKey *rsa.PrivateKey
wechatPayPublicKey *rsa.PublicKey
}
func (c *MchConfig) MchId() string {
return c.mchId
}
func (c *MchConfig) CertificateSerialNo() string {
return c.certificateSerialNo
}
func (c *MchConfig) PrivateKey() *rsa.PrivateKey {
return c.privateKey
}
func (c *MchConfig) WechatPayPublicKeyId() string {
return c.wechatPayPublicKeyId
}
func (c *MchConfig) WechatPayPublicKey() *rsa.PublicKey {
return c.wechatPayPublicKey
}
func CreateMchConfig(
mchId string,
certificateSerialNo string,
privateKeyFilePath string,
wechatPayPublicKeyId string,
wechatPayPublicKeyFilePath string,
) (*MchConfig, error) {
mchConfig := &MchConfig{
mchId: mchId,
certificateSerialNo: certificateSerialNo,
privateKeyFilePath: privateKeyFilePath,
wechatPayPublicKeyId: wechatPayPublicKeyId,
wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath,
}
privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath)
if err != nil {
return nil, err
}
mchConfig.privateKey = privateKey
wechatPayPublicKey, err := LoadPublicKeyWithPath(mchConfig.wechatPayPublicKeyFilePath)
if err != nil {
return nil, err
}
mchConfig.wechatPayPublicKey = wechatPayPublicKey
return mchConfig, nil
}
func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) {
block, _ := pem.Decode([]byte(privateKeyStr))
if block == nil {
return nil, fmt.Errorf("decode private key err")
}
if block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse private key err:%s", err.Error())
}
privateKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("not a RSA private key")
}
return privateKey, nil
}
func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) {
block, _ := pem.Decode([]byte(publicKeyStr))
if block == nil {
return nil, errors.New("decode public key error")
}
if block.Type != "PUBLIC KEY" {
return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY")
}
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse public key err:%s", err.Error())
}
publicKey, ok := key.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr)
}
return publicKey, nil
}
func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
privateKeyBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read private pem file err:%s", err.Error())
}
return LoadPrivateKey(string(privateKeyBytes))
}
func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) {
publicKeyBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read certificate pem file err:%s", err.Error())
}
return LoadPublicKey(string(publicKeyBytes))
}
func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error) {
if publicKey == nil {
return "", fmt.Errorf("you should input *rsa.PublicKey")
}
ciphertextByte, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(message), nil)
if err != nil {
return "", fmt.Errorf("encrypt message with public key err:%s", err.Error())
}
ciphertext = base64.StdEncoding.EncodeToString(ciphertextByte)
return ciphertext, nil
}
func DecryptAES256GCM(aesKey, associatedData, nonce, ciphertext string) (plaintext string, err error) {
decodedCiphertext, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
c, err := aes.NewCipher([]byte(aesKey))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return "", err
}
dataBytes, err := gcm.Open(nil, []byte(nonce), decodedCiphertext, []byte(associatedData))
if err != nil {
return "", err
}
return string(dataBytes), nil
}
func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) {
if privateKey == nil {
return "", fmt.Errorf("private key should not be nil")
}
h := crypto.Hash.New(crypto.SHA256)
_, err = h.Write([]byte(source))
if err != nil {
return "", nil
}
hashed := h.Sum(nil)
signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signatureByte), nil
}
func VerifySHA256WithRSA(source string, signature string, publicKey *rsa.PublicKey) error {
if publicKey == nil {
return fmt.Errorf("public key should not be nil")
}
sigBytes, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return fmt.Errorf("verify failed: signature is not base64 encoded")
}
hashed := sha256.Sum256([]byte(source))
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], sigBytes)
if err != nil {
return fmt.Errorf("verify signature with public key error:%s", err.Error())
}
return nil
}
func GenerateNonce() (string, error) {
const (
NonceSymbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
NonceLength = 32
)
bytes := make([]byte, NonceLength)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
symbolsByteLength := byte(len(NonceSymbols))
for i, b := range bytes {
bytes[i] = NonceSymbols[b%symbolsByteLength]
}
return string(bytes), nil
}
func BuildAuthorization(
mchid string,
certificateSerialNo string,
privateKey *rsa.PrivateKey,
method string,
canonicalURL string,
body []byte,
) (string, error) {
const (
SignatureMessageFormat = "%s\n%s\n%d\n%s\n%s\n"
HeaderAuthorizationFormat = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
)
nonce, err := GenerateNonce()
if err != nil {
return "", err
}
timestamp := time.Now().Unix()
message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body)
signature, err := SignSHA256WithRSA(message, privateKey)
if err != nil {
return "", err
}
authorization := fmt.Sprintf(
HeaderAuthorizationFormat,
mchid, nonce, timestamp, certificateSerialNo, signature,
)
return authorization, nil
}
func ExtractResponseBody(response *http.Response) ([]byte, error) {
if response.Body == nil {
return nil, nil
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("read response body err:[%s]", err.Error())
}
response.Body = io.NopCloser(bytes.NewBuffer(body))
return body, nil
}
const (
WechatPayTimestamp = "Wechatpay-Timestamp"
WechatPayNonce = "Wechatpay-Nonce"
WechatPaySignature = "Wechatpay-Signature"
WechatPaySerial = "Wechatpay-Serial"
RequestID = "Request-Id"
)
func validateWechatPaySignature(
wechatpayPublicKeyId string,
wechatpayPublicKey *rsa.PublicKey,
headers *http.Header,
body []byte,
) error {
timestampStr := headers.Get(WechatPayTimestamp)
serialNo := headers.Get(WechatPaySerial)
signature := headers.Get(WechatPaySignature)
nonce := headers.Get(WechatPayNonce)
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp: %w", err)
}
if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute {
return fmt.Errorf("timestamp expired: %d", timestamp)
}
if serialNo != wechatpayPublicKeyId {
return fmt.Errorf(
"serial-no mismatch: got %s, expected %s",
serialNo,
wechatpayPublicKeyId,
)
}
message := fmt.Sprintf("%s\n%s\n%s\n", timestampStr, nonce, body)
if err := VerifySHA256WithRSA(message, signature, wechatpayPublicKey); err != nil {
return fmt.Errorf("invalid signature: %v", err)
}
return nil
}
func ValidateResponse(
wechatpayPublicKeyId string,
wechatpayPublicKey *rsa.PublicKey,
headers *http.Header,
body []byte,
) error {
if err := validateWechatPaySignature(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
return fmt.Errorf("validate response err: %w, RequestID: %s", err, headers.Get(RequestID))
}
return nil
}
func validateNotification(
wechatpayPublicKeyId string,
wechatpayPublicKey *rsa.PublicKey,
headers *http.Header,
body []byte,
) error {
if err := validateWechatPaySignature(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
return fmt.Errorf("validate notification err: %w", err)
}
return nil
}
type Resource struct {
Algorithm string `json:"algorithm"`
Ciphertext string `json:"ciphertext"`
AssociatedData string `json:"associated_data"`
Nonce string `json:"nonce"`
OriginalType string `json:"original_type"`
}
type Notification struct {
ID string `json:"id"`
CreateTime *time.Time `json:"create_time"`
EventType string `json:"event_type"`
ResourceType string `json:"resource_type"`
Resource *Resource `json:"resource"`
Summary string `json:"summary"`
Plaintext string
}
func (c *Notification) validate() error {
if c.Resource == nil {
return errors.New("resource is nil")
}
if c.Resource.Algorithm != "AEAD_AES_256_GCM" {
return fmt.Errorf("unsupported algorithm: %s", c.Resource.Algorithm)
}
if c.Resource.Ciphertext == "" {
return errors.New("ciphertext is empty")
}
if c.Resource.AssociatedData == "" {
return errors.New("associated_data is empty")
}
if c.Resource.Nonce == "" {
return errors.New("nonce is empty")
}
if c.Resource.OriginalType == "" {
return fmt.Errorf("original_type is empty")
}
return nil
}
func (c *Notification) decrypt(apiv3Key string) error {
if err := c.validate(); err != nil {
return fmt.Errorf("notification format err: %w", err)
}
plaintext, err := DecryptAES256GCM(
apiv3Key,
c.Resource.AssociatedData,
c.Resource.Nonce,
c.Resource.Ciphertext,
)
if err != nil {
return fmt.Errorf("notification decrypt err: %w", err)
}
c.Plaintext = plaintext
return nil
}
func ParseNotification(
wechatpayPublicKeyId string,
wechatpayPublicKey *rsa.PublicKey,
apiv3Key string,
headers *http.Header,
body []byte,
) (*Notification, error) {
if err := validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
return nil, err
}
notification := &Notification{}
if err := json.Unmarshal(body, notification); err != nil {
return nil, fmt.Errorf("parse notification err: %w", err)
}
if err := notification.decrypt(apiv3Key); err != nil {
return nil, fmt.Errorf("notification decrypt err: %w", err)
}
return notification, nil
}
type ApiException struct {
statusCode int
header http.Header
body []byte
errorCode string
errorMessage string
}
func (c *ApiException) Error() string {
buf := bytes.NewBuffer(nil)
buf.WriteString(fmt.Sprintf("api error:[StatusCode: %d, Body: %s", c.statusCode, string(c.body)))
if len(c.header) > 0 {
buf.WriteString(" Header: ")
for key, value := range c.header {
buf.WriteString(fmt.Sprintf("\n - %v=%v", key, value))
}
buf.WriteString("\n")
}
buf.WriteString("]")
return buf.String()
}
func (c *ApiException) StatusCode() int {
return c.statusCode
}
func (c *ApiException) Header() http.Header {
return c.header
}
func (c *ApiException) Body() []byte {
return c.body
}
func (c *ApiException) ErrorCode() string {
return c.errorCode
}
func (c *ApiException) ErrorMessage() string {
return c.errorMessage
}
func NewApiException(statusCode int, header http.Header, body []byte) error {
ret := &ApiException{
statusCode: statusCode,
header: header,
body: body,
}
bodyObject := map[string]interface{}{}
if err := json.Unmarshal(body, &bodyObject); err == nil {
if val, ok := bodyObject["code"]; ok {
ret.errorCode = val.(string)
}
if val, ok := bodyObject["message"]; ok {
ret.errorMessage = val.(string)
}
}
return ret
}
func Time(t time.Time) *time.Time {
return &t
}
func String(s string) *string {
return &s
}
func Bytes(b []byte) *[]byte {
return &b
}
func Bool(b bool) *bool {
return &b
}
func Float64(f float64) *float64 {
return &f
}
func Float32(f float32) *float32 {
return &f
}
func Int64(i int64) *int64 {
return &i
}
func Int32(i int32) *int32 {
return &i
}
func generateHashFromStream(reader io.Reader, hashFunc func() hash.Hash, algorithmName string) (string, error) {
hash := hashFunc()
if _, err := io.Copy(hash, reader); err != nil {
return "", fmt.Errorf("failed to read stream for %s: %w", algorithmName, err)
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}
func GenerateSHA256FromStream(reader io.Reader) (string, error) {
return generateHashFromStream(reader, sha256.New, "SHA256")
}
func GenerateSHA1FromStream(reader io.Reader) (string, error) {
return generateHashFromStream(reader, sha1.New, "SHA1")
}
func GenerateSM3FromStream(reader io.Reader) (string, error) {
h := sm3.New()
if _, err := io.Copy(h, reader); err != nil {
return "", fmt.Errorf("failed to read stream for SM3: %w", err)
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

View File

@@ -0,0 +1,191 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &UnionApiv3AppPrepayRequest{
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
CombineOutTradeNo: wxpay_utility.String("20150806125345"),
CombineMchid: wxpay_utility.String("1900000109"),
SceneInfo: &UnionSceneInfo{
DeviceId: wxpay_utility.String("POS1:1"),
PayerClientIp: wxpay_utility.String("14.17.22.32"),
},
SubOrders: []UnionSubOrder{
UnionSubOrder{
Mchid: wxpay_utility.String("1230000109"),
OutTradeNo: wxpay_utility.String("20150806125346"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("深圳分店"),
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
Detail: wxpay_utility.String("买单费用"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
UnionSubOrder{
Mchid: wxpay_utility.String("1230000119"),
OutTradeNo: wxpay_utility.String("20150806125347"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("广州分店"),
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
Detail: wxpay_utility.String("买单费用"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
},
CombinePayerInfo: &UnionAppPayerInfo{
Openid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
},
TimeExpire: wxpay_utility.Time(time.Now()),
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
}
response, err := UnionAppPrepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func UnionAppPrepay(config *wxpay_utility.MchConfig, request *UnionApiv3AppPrepayRequest) (response *UnionApiv3AppPrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/combine-transactions/app"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &UnionApiv3AppPrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type UnionApiv3AppPrepayRequest struct {
CombineAppid *string `json:"combine_appid,omitempty"`
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
CombineMchid *string `json:"combine_mchid,omitempty"`
SceneInfo *UnionSceneInfo `json:"scene_info,omitempty"`
SubOrders []UnionSubOrder `json:"sub_orders,omitempty"`
CombinePayerInfo *UnionAppPayerInfo `json:"combine_payer_info,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
TradeScenario *string `json:"trade_scenario,omitempty"`
}
type UnionApiv3AppPrepayResponse struct {
PrepayId *string `json:"prepay_id,omitempty"`
}
type UnionSceneInfo struct {
DeviceId *string `json:"device_id,omitempty"`
PayerClientIp *string `json:"payer_client_ip,omitempty"`
}
type UnionSubOrder struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
Amount *UnionAmountInfo `json:"amount,omitempty"`
Attach *string `json:"attach,omitempty"`
Description *string `json:"description,omitempty"`
Detail *string `json:"detail,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
}
type UnionAppPayerInfo struct {
Openid *string `json:"openid,omitempty"`
}
type UnionAmountInfo struct {
TotalAmount *int64 `json:"total_amount,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type UnionSettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}

View File

@@ -0,0 +1,128 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &UnionCloseRequest{
CombineOutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
SubOrders: []UnionCloseSubOrder{UnionCloseSubOrder{
Mchid: wxpay_utility.String("1900000109"),
OutTradeNo: wxpay_utility.String("20150806125346"),
}},
}
err = UnionClose(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Println("请求成功")
}
func UnionClose(config *wxpay_utility.MchConfig, request *UnionCloseRequest) (err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{combine_out_trade_no}", url.PathEscape(*request.CombineOutTradeNo), -1)
reqBody, err := json.Marshal(request)
if err != nil {
return err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return err
}
return nil
} else {
return wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type UnionCloseRequest struct {
CombineAppid *string `json:"combine_appid,omitempty"`
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
SubOrders []UnionCloseSubOrder `json:"sub_orders,omitempty"`
}
func (o *UnionCloseRequest) MarshalJSON() ([]byte, error) {
type Alias UnionCloseRequest
a := &struct {
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
CombineOutTradeNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type UnionCloseSubOrder struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
}

View File

@@ -0,0 +1,195 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &UnionApiv3H5PrepayRequest{
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
CombineOutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
CombineMchid: wxpay_utility.String("1230000109"),
SceneInfo: &UnionH5SceneInfo{
PayerClientIp: wxpay_utility.String("14.23.150.211"),
DeviceId: wxpay_utility.String("013467007045764"),
H5Info: &UnionH5Info{
Type: wxpay_utility.String("iOS"),
AppName: wxpay_utility.String("王者荣耀"),
AppUrl: wxpay_utility.String("https://pay.qq.com"),
BundleId: wxpay_utility.String("com.tencent.wzryiOS"),
PackageName: wxpay_utility.String("com.tencent.tmgp.sgame"),
},
},
SubOrders: []UnionCommonSubOrder{
UnionCommonSubOrder{
Mchid: wxpay_utility.String("1230000109"),
OutTradeNo: wxpay_utility.String("20150806125346"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("深圳分店"),
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
UnionCommonSubOrder{
Mchid: wxpay_utility.String("1230000119"),
OutTradeNo: wxpay_utility.String("20150806125347"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("广州分店"),
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
},
TimeExpire: wxpay_utility.Time(time.Now()),
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
}
response, err := UnionH5Prepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func UnionH5Prepay(config *wxpay_utility.MchConfig, request *UnionApiv3H5PrepayRequest) (response *UnionApiv3H5PrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/combine-transactions/h5"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &UnionApiv3H5PrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type UnionApiv3H5PrepayRequest struct {
CombineAppid *string `json:"combine_appid,omitempty"`
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
CombineMchid *string `json:"combine_mchid,omitempty"`
SceneInfo *UnionH5SceneInfo `json:"scene_info,omitempty"`
SubOrders []UnionCommonSubOrder `json:"sub_orders,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
}
type UnionApiv3H5PrepayResponse struct {
H5Url *string `json:"h5_url,omitempty"`
}
type UnionH5SceneInfo struct {
PayerClientIp *string `json:"payer_client_ip,omitempty"`
DeviceId *string `json:"device_id,omitempty"`
H5Info *UnionH5Info `json:"h5_info,omitempty"`
}
type UnionCommonSubOrder struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
Amount *UnionAmountInfo `json:"amount,omitempty"`
Attach *string `json:"attach,omitempty"`
Description *string `json:"description,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
}
type UnionH5Info struct {
Type *string `json:"type,omitempty"`
AppName *string `json:"app_name,omitempty"`
AppUrl *string `json:"app_url,omitempty"`
BundleId *string `json:"bundle_id,omitempty"`
PackageName *string `json:"package_name,omitempty"`
}
type UnionAmountInfo struct {
TotalAmount *int64 `json:"total_amount,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type UnionSettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}

View File

@@ -0,0 +1,191 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &UnionApiv3JsapiPrepayRequest{
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
CombineMchid: wxpay_utility.String("1230000109"),
CombineOutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
CombinePayerInfo: &UnionPayerInfo{
Openid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
},
SceneInfo: &UnionSceneInfo{
DeviceId: wxpay_utility.String("POS1:1"),
PayerClientIp: wxpay_utility.String("14.17.22.32"),
},
SubOrders: []UnionSubOrder{
UnionSubOrder{
Mchid: wxpay_utility.String("1230000109"),
OutTradeNo: wxpay_utility.String("20150806125346"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("深圳分店"),
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
Detail: wxpay_utility.String("买单费用"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
UnionSubOrder{
Mchid: wxpay_utility.String("1230000119"),
OutTradeNo: wxpay_utility.String("20150806125347"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("广州分店"),
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
Detail: wxpay_utility.String("买单费用"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
},
TimeExpire: wxpay_utility.Time(time.Now()),
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
}
response, err := UnionJsapiPrepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func UnionJsapiPrepay(config *wxpay_utility.MchConfig, request *UnionApiv3JsapiPrepayRequest) (response *UnionApiv3JsapiPrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/combine-transactions/jsapi"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &UnionApiv3JsapiPrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type UnionApiv3JsapiPrepayRequest struct {
CombineAppid *string `json:"combine_appid,omitempty"`
CombineMchid *string `json:"combine_mchid,omitempty"`
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
CombinePayerInfo *UnionPayerInfo `json:"combine_payer_info,omitempty"`
SceneInfo *UnionSceneInfo `json:"scene_info,omitempty"`
SubOrders []UnionSubOrder `json:"sub_orders,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
}
type UnionApiv3JsapiPrepayResponse struct {
PrepayId *string `json:"prepay_id,omitempty"`
}
type UnionPayerInfo struct {
Openid *string `json:"openid,omitempty"`
SubOpenid *string `json:"sub_openid,omitempty"`
}
type UnionSceneInfo struct {
DeviceId *string `json:"device_id,omitempty"`
PayerClientIp *string `json:"payer_client_ip,omitempty"`
}
type UnionSubOrder struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
Amount *UnionAmountInfo `json:"amount,omitempty"`
Attach *string `json:"attach,omitempty"`
Description *string `json:"description,omitempty"`
Detail *string `json:"detail,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
}
type UnionAmountInfo struct {
TotalAmount *int64 `json:"total_amount,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type UnionSettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}

View File

@@ -0,0 +1,179 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &UnionApiv3NativePrepayRequest{
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
CombineOutTradeNo: wxpay_utility.String("20150806125346"),
CombineMchid: wxpay_utility.String("1900000109"),
SceneInfo: &UnionSceneInfo{
DeviceId: wxpay_utility.String("POS1:1"),
PayerClientIp: wxpay_utility.String("14.17.22.32"),
},
SubOrders: []UnionCommonSubOrder{
UnionCommonSubOrder{
Mchid: wxpay_utility.String("1230000109"),
OutTradeNo: wxpay_utility.String("20150806125346"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("深圳分店"),
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
UnionCommonSubOrder{
Mchid: wxpay_utility.String("1230000119"),
OutTradeNo: wxpay_utility.String("20150806125347"),
Amount: &UnionAmountInfo{
TotalAmount: wxpay_utility.Int64(10),
Currency: wxpay_utility.String("CNY"),
},
Attach: wxpay_utility.String("广州分店"),
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &UnionSettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
},
},
TimeExpire: wxpay_utility.Time(time.Now()),
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
}
response, err := UnionNativePrepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func UnionNativePrepay(config *wxpay_utility.MchConfig, request *UnionApiv3NativePrepayRequest) (response *UnionApiv3NativePrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/combine-transactions/native"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &UnionApiv3NativePrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type UnionApiv3NativePrepayRequest struct {
CombineAppid *string `json:"combine_appid,omitempty"`
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
CombineMchid *string `json:"combine_mchid,omitempty"`
SceneInfo *UnionSceneInfo `json:"scene_info,omitempty"`
SubOrders []UnionCommonSubOrder `json:"sub_orders,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
}
type UnionApiv3NativePrepayResponse struct {
CodeUrl *string `json:"code_url,omitempty"`
}
type UnionSceneInfo struct {
DeviceId *string `json:"device_id,omitempty"`
PayerClientIp *string `json:"payer_client_ip,omitempty"`
}
type UnionCommonSubOrder struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
Amount *UnionAmountInfo `json:"amount,omitempty"`
Attach *string `json:"attach,omitempty"`
Description *string `json:"description,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
}
type UnionAmountInfo struct {
TotalAmount *int64 `json:"total_amount,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type UnionSettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}

View File

@@ -0,0 +1,178 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &UnionQueryByOutTradeNoRequest{
CombineOutTradeNo: wxpay_utility.String("P20150806125346"),
}
response, err := UnionQueryByOutTradeNo(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func UnionQueryByOutTradeNo(config *wxpay_utility.MchConfig, request *UnionQueryByOutTradeNoRequest) (response *UnionApiv3UnionQueryResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{combine_out_trade_no}", url.PathEscape(*request.CombineOutTradeNo), -1)
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &UnionApiv3UnionQueryResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type UnionQueryByOutTradeNoRequest struct {
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
}
func (o *UnionQueryByOutTradeNoRequest) MarshalJSON() ([]byte, error) {
type Alias UnionQueryByOutTradeNoRequest
a := &struct {
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
CombineOutTradeNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type UnionApiv3UnionQueryResponse struct {
CombineAppid *string `json:"combine_appid,omitempty"`
CombineMchid *string `json:"combine_mchid,omitempty"`
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
CombinePayerInfo *UnionCommRespPayerInfo `json:"combine_payer_info,omitempty"`
SceneInfo *UnionCommRespSceneInfo `json:"scene_info,omitempty"`
SubOrders []UnionSubOrder `json:"sub_orders,omitempty"`
}
type UnionCommRespPayerInfo struct {
Openid *string `json:"openid,omitempty"`
}
type UnionCommRespSceneInfo struct {
DeviceId *string `json:"device_id,omitempty"`
}
type UnionSubOrder struct {
Mchid *string `json:"mchid,omitempty"`
SubMchid *string `json:"sub_mchid,omitempty"`
SubAppid *string `json:"sub_appid,omitempty"`
SubOpenid *string `json:"sub_openid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
TradeType *string `json:"trade_type,omitempty"`
TradeState *string `json:"trade_state,omitempty"`
BankType *string `json:"bank_type,omitempty"`
Attach *string `json:"attach,omitempty"`
SuccessTime *string `json:"success_time,omitempty"`
Amount *UnionCommRespAmountInfo `json:"amount,omitempty"`
PromotionDetail []UnionPromotionDetail `json:"promotion_detail,omitempty"`
}
type UnionCommRespAmountInfo struct {
TotalAmount *int64 `json:"total_amount,omitempty"`
PayerAmount *int64 `json:"payer_amount,omitempty"`
Currency *string `json:"currency,omitempty"`
PayerCurrency *string `json:"payer_currency,omitempty"`
SettlementRate *int64 `json:"settlement_rate,omitempty"`
}
type UnionPromotionDetail struct {
CouponId *string `json:"coupon_id,omitempty"`
Name *string `json:"name,omitempty"`
Scope *string `json:"scope,omitempty"`
Type *string `json:"type,omitempty"`
Amount *int64 `json:"amount,omitempty"`
StockId *string `json:"stock_id,omitempty"`
WechatpayContribute *int64 `json:"wechatpay_contribute,omitempty"`
MerchantContribute *int64 `json:"merchant_contribute,omitempty"`
OtherContribute *int64 `json:"other_contribute,omitempty"`
Currency *string `json:"currency,omitempty"`
GoodsDetail []GoodsDetailInPromotion `json:"goods_detail,omitempty"`
}
type GoodsDetailInPromotion struct {
GoodsId *string `json:"goods_id,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
DiscountAmount *int64 `json:"discount_amount,omitempty"`
GoodsRemark *string `json:"goods_remark,omitempty"`
}

View File

@@ -0,0 +1,156 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &AddReceiverRequest{
Appid: wxpay_utility.String("wx8888888888888888"),
Type: RECEIVERTYPE_MERCHANT_ID.Ptr(),
Account: wxpay_utility.String("86693852"),
Name: wxpay_utility.String("hu89ohu89ohu89o"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
RelationType: RECEIVERRELATIONTYPE_STORE.Ptr(),
CustomRelation: wxpay_utility.String("代理商"),
}
response, err := AddReceiver(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func AddReceiver(config *wxpay_utility.MchConfig, request *AddReceiverRequest) (response *AddReceiverResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/receivers/add"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &AddReceiverResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type AddReceiverRequest struct {
Appid *string `json:"appid,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
RelationType *ReceiverRelationType `json:"relation_type,omitempty"`
CustomRelation *string `json:"custom_relation,omitempty"`
}
type AddReceiverResponse struct {
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
RelationType *ReceiverRelationType `json:"relation_type,omitempty"`
CustomRelation *string `json:"custom_relation,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType {
return &e
}
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
)
type ReceiverRelationType string
func (e ReceiverRelationType) Ptr() *ReceiverRelationType {
return &e
}
const (
RECEIVERRELATIONTYPE_STORE ReceiverRelationType = "STORE"
RECEIVERRELATIONTYPE_STAFF ReceiverRelationType = "STAFF"
RECEIVERRELATIONTYPE_STORE_OWNER ReceiverRelationType = "STORE_OWNER"
RECEIVERRELATIONTYPE_PARTNER ReceiverRelationType = "PARTNER"
RECEIVERRELATIONTYPE_HEADQUARTER ReceiverRelationType = "HEADQUARTER"
RECEIVERRELATIONTYPE_BRAND ReceiverRelationType = "BRAND"
RECEIVERRELATIONTYPE_DISTRIBUTOR ReceiverRelationType = "DISTRIBUTOR"
RECEIVERRELATIONTYPE_USER ReceiverRelationType = "USER"
RECEIVERRELATIONTYPE_SUPPLIER ReceiverRelationType = "SUPPLIER"
RECEIVERRELATIONTYPE_CUSTOM ReceiverRelationType = "CUSTOM"
)
删除分账接收方

View File

@@ -0,0 +1,178 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &CreateOrderRequest{
Appid: wxpay_utility.String("wx8888888888888888"),
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
OutOrderNo: wxpay_utility.String("P20150806125346"),
Receivers: []CreateOrderReceiver{CreateOrderReceiver{
Type: wxpay_utility.String("MERCHANT_ID"),
Account: wxpay_utility.String("86693852"),
Name: wxpay_utility.String("hu89ohu89ohu89o"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
Amount: wxpay_utility.Int64(888),
Description: wxpay_utility.String("分给商户A"),
}},
UnfreezeUnsplit: wxpay_utility.Bool(true),
}
response, err := CreateOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func CreateOrder(config *wxpay_utility.MchConfig, request *CreateOrderRequest) (response *OrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/orders"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &OrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type CreateOrderRequest struct {
Appid *string `json:"appid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
Receivers []CreateOrderReceiver `json:"receivers,omitempty"`
UnfreezeUnsplit *bool `json:"unfreeze_unsplit,omitempty"`
}
type OrdersEntity struct {
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OrderId *string `json:"order_id,omitempty"`
State *OrderStatus `json:"state,omitempty"`
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
}
type CreateOrderReceiver struct {
Type *string `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
}
type OrderStatus string
func (e OrderStatus) Ptr() *OrderStatus { return &e }
const (
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
)
type OrderReceiverDetail struct {
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Result *DetailStatus `json:"result,omitempty"`
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
DetailId *string `json:"detail_id,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType { return &e }
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
)
type DetailStatus string
func (e DetailStatus) Ptr() *DetailStatus { return &e }
const (
DETAILSTATUS_PENDING DetailStatus = "PENDING"
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
)
type DetailFailReason string
func (e DetailFailReason) Ptr() *DetailFailReason { return &e }
const (
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
)

View File

@@ -0,0 +1,159 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &CreateReturnOrderRequest{
OrderId: wxpay_utility.String("3008450740201411110007820472"),
OutOrderNo: wxpay_utility.String("P20150806125346"),
OutReturnNo: wxpay_utility.String("R20190516001"),
ReturnMchid: wxpay_utility.String("86693852"),
Amount: wxpay_utility.Int64(10),
Description: wxpay_utility.String("用户退款"),
}
response, err := CreateReturnOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func CreateReturnOrder(config *wxpay_utility.MchConfig, request *CreateReturnOrderRequest) (response *ReturnOrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/return-orders"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &ReturnOrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type CreateReturnOrderRequest struct {
OrderId *string `json:"order_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OutReturnNo *string `json:"out_return_no,omitempty"`
ReturnMchid *string `json:"return_mchid,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
}
type ReturnOrdersEntity struct {
OrderId *string `json:"order_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OutReturnNo *string `json:"out_return_no,omitempty"`
ReturnId *string `json:"return_id,omitempty"`
ReturnMchid *string `json:"return_mchid,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Result *ReturnOrderStatus `json:"result,omitempty"`
FailReason *ReturnOrderFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
}
type ReturnOrderStatus string
func (e ReturnOrderStatus) Ptr() *ReturnOrderStatus {
return &e
}
const (
RETURNORDERSTATUS_PROCESSING ReturnOrderStatus = "PROCESSING"
RETURNORDERSTATUS_SUCCESS ReturnOrderStatus = "SUCCESS"
RETURNORDERSTATUS_FAILED ReturnOrderStatus = "FAILED"
)
type ReturnOrderFailReason string
func (e ReturnOrderFailReason) Ptr() *ReturnOrderFailReason {
return &e
}
const (
RETURNORDERFAILREASON_ACCOUNT_ABNORMAL ReturnOrderFailReason = "ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_BALANCE_NOT_ENOUGH ReturnOrderFailReason = "BALANCE_NOT_ENOUGH"
RETURNORDERFAILREASON_TIME_OUT_CLOSED ReturnOrderFailReason = "TIME_OUT_CLOSED"
RETURNORDERFAILREASON_PAYER_ACCOUNT_ABNORMAL ReturnOrderFailReason = "PAYER_ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_INVALID_REQUEST ReturnOrderFailReason = "INVALID_REQUEST"
)
查询分账回退结果

View File

@@ -0,0 +1,128 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &DeleteReceiverRequest{
Appid: wxpay_utility.String("wx8888888888888888"),
Type: RECEIVERTYPE_MERCHANT_ID.Ptr(),
Account: wxpay_utility.String("1900000109"),
}
response, err := DeleteReceiver(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func DeleteReceiver(config *wxpay_utility.MchConfig, request *DeleteReceiverRequest) (response *DeleteReceiverResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/receivers/delete"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &DeleteReceiverResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type DeleteReceiverRequest struct {
Appid *string `json:"appid,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
}
type DeleteReceiverResponse struct {
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType {
return &e
}
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
)
申请分账账单

View File

@@ -0,0 +1,198 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryOrderRequest{
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
OutOrderNo: wxpay_utility.String("P20150806125346"),
}
response, err := QueryOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func QueryOrder(config *wxpay_utility.MchConfig, request *QueryOrderRequest) (response *OrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/orders/{out_order_no}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_order_no}", url.PathEscape(*request.OutOrderNo), -1)
query := reqUrl.Query()
if request.TransactionId != nil {
query.Add("transaction_id", *request.TransactionId)
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &OrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type QueryOrderRequest struct {
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
}
func (o *QueryOrderRequest) MarshalJSON() ([]byte, error) {
type Alias QueryOrderRequest
a := &struct {
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
TransactionId: nil,
OutOrderNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type OrdersEntity struct {
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OrderId *string `json:"order_id,omitempty"`
State *OrderStatus `json:"state,omitempty"`
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
}
type OrderStatus string
func (e OrderStatus) Ptr() *OrderStatus {
return &e
}
const (
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
)
type OrderReceiverDetail struct {
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Result *DetailStatus `json:"result,omitempty"`
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
DetailId *string `json:"detail_id,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType {
return &e
}
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
)
type DetailStatus string
func (e DetailStatus) Ptr() *DetailStatus {
return &e
}
const (
DETAILSTATUS_PENDING DetailStatus = "PENDING"
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
)
type DetailFailReason string
func (e DetailFailReason) Ptr() *DetailFailReason {
return &e
}
const (
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
)
请求分账回退

View File

@@ -0,0 +1,122 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryOrderAmountRequest{
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
}
response, err := QueryOrderAmount(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func QueryOrderAmount(config *wxpay_utility.MchConfig, request *QueryOrderAmountRequest) (response *QueryOrderAmountResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/transactions/{transaction_id}/amounts"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{transaction_id}", url.PathEscape(*request.TransactionId), -1)
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &QueryOrderAmountResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type QueryOrderAmountRequest struct {
TransactionId *string `json:"transaction_id,omitempty"`
}
func (o *QueryOrderAmountRequest) MarshalJSON() ([]byte, error) {
type Alias QueryOrderAmountRequest
a := &struct {
TransactionId *string `json:"transaction_id,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
TransactionId: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type QueryOrderAmountResponse struct {
TransactionId *string `json:"transaction_id,omitempty"`
UnsplitAmount *int64 `json:"unsplit_amount,omitempty"`
}
添加分账接收方

View File

@@ -0,0 +1,167 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryReturnOrderRequest{
OutReturnNo: wxpay_utility.String("R20190516001"),
OutOrderNo: wxpay_utility.String("P20190806125346"),
}
response, err := QueryReturnOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func QueryReturnOrder(config *wxpay_utility.MchConfig, request *QueryReturnOrderRequest) (response *ReturnOrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/return-orders/{out_return_no}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_return_no}", url.PathEscape(*request.OutReturnNo), -1)
query := reqUrl.Query()
if request.OutOrderNo != nil {
query.Add("out_order_no", *request.OutOrderNo)
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &ReturnOrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type QueryReturnOrderRequest struct {
OutReturnNo *string `json:"out_return_no,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
}
func (o *QueryReturnOrderRequest) MarshalJSON() ([]byte, error) {
type Alias QueryReturnOrderRequest
a := &struct {
OutReturnNo *string `json:"out_return_no,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
OutReturnNo: nil,
OutOrderNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type ReturnOrdersEntity struct {
OrderId *string `json:"order_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OutReturnNo *string `json:"out_return_no,omitempty"`
ReturnId *string `json:"return_id,omitempty"`
ReturnMchid *string `json:"return_mchid,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Result *ReturnOrderStatus `json:"result,omitempty"`
FailReason *ReturnOrderFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
}
type ReturnOrderStatus string
func (e ReturnOrderStatus) Ptr() *ReturnOrderStatus {
return &e
}
const (
RETURNORDERSTATUS_PROCESSING ReturnOrderStatus = "PROCESSING"
RETURNORDERSTATUS_SUCCESS ReturnOrderStatus = "SUCCESS"
RETURNORDERSTATUS_FAILED ReturnOrderStatus = "FAILED"
)
type ReturnOrderFailReason string
func (e ReturnOrderFailReason) Ptr() *ReturnOrderFailReason {
return &e
}
const (
RETURNORDERFAILREASON_ACCOUNT_ABNORMAL ReturnOrderFailReason = "ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_BALANCE_NOT_ENOUGH ReturnOrderFailReason = "BALANCE_NOT_ENOUGH"
RETURNORDERFAILREASON_TIME_OUT_CLOSED ReturnOrderFailReason = "TIME_OUT_CLOSED"
RETURNORDERFAILREASON_PAYER_ACCOUNT_ABNORMAL ReturnOrderFailReason = "PAYER_ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_INVALID_REQUEST ReturnOrderFailReason = "INVALID_REQUEST"
)
解冻剩余资金

View File

@@ -0,0 +1,151 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &SplitBillRequest{
BillDate: wxpay_utility.String("2019-06-11"),
TarType: SPLITBILLTARTYPE_GZIP.Ptr(),
}
response, err := SplitBill(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func SplitBill(config *wxpay_utility.MchConfig, request *SplitBillRequest) (response *SplitBillResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/bills"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
query := reqUrl.Query()
if request.BillDate != nil {
query.Add("bill_date", *request.BillDate)
}
if request.TarType != nil {
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &SplitBillResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type SplitBillRequest struct {
BillDate *string `json:"bill_date,omitempty"`
TarType *SplitBillTarType `json:"tar_type,omitempty"`
}
func (o *SplitBillRequest) MarshalJSON() ([]byte, error) {
type Alias SplitBillRequest
a := &struct {
BillDate *string `json:"bill_date,omitempty"`
TarType *SplitBillTarType `json:"tar_type,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
BillDate: nil,
TarType: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type SplitBillResponse struct {
HashType *SplitBillHashType `json:"hash_type,omitempty"`
HashValue *string `json:"hash_value,omitempty"`
DownloadUrl *string `json:"download_url,omitempty"`
}
type SplitBillTarType string
func (e SplitBillTarType) Ptr() *SplitBillTarType {
return &e
}
const (
SPLITBILLTARTYPE_GZIP SplitBillTarType = "GZIP"
)
type SplitBillHashType string
func (e SplitBillHashType) Ptr() *SplitBillHashType {
return &e
}
const (
SPLITBILLHASHTYPE_SHA1 SplitBillHashType = "SHA1"
)

View File

@@ -0,0 +1,184 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &UnfreezeOrderRequest{
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
OutOrderNo: wxpay_utility.String("P20150806125346"),
Description: wxpay_utility.String("解冻全部剩余资金"),
}
response, err := UnfreezeOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func UnfreezeOrder(config *wxpay_utility.MchConfig, request *UnfreezeOrderRequest) (response *OrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/orders/unfreeze"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &OrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type UnfreezeOrderRequest struct {
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
Description *string `json:"description,omitempty"`
}
type OrdersEntity struct {
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OrderId *string `json:"order_id,omitempty"`
State *OrderStatus `json:"state,omitempty"`
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
}
type OrderStatus string
func (e OrderStatus) Ptr() *OrderStatus {
return &e
}
const (
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
)
type OrderReceiverDetail struct {
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Result *DetailStatus `json:"result,omitempty"`
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
DetailId *string `json:"detail_id,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType {
return &e
}
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
)
type DetailStatus string
func (e DetailStatus) Ptr() *DetailStatus {
return &e
}
const (
DETAILSTATUS_PENDING DetailStatus = "PENDING"
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
)
type DetailFailReason string
func (e DetailFailReason) Ptr() *DetailFailReason {
return &e
}
const (
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
)
查询剩余待分金额

View File

@@ -0,0 +1,196 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &CommonPrepayRequest{
Appid: wxpay_utility.String("wxd678efh567hg6787"),
Mchid: wxpay_utility.String("1230000109"),
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
TimeExpire: wxpay_utility.Time(time.Now()),
Attach: wxpay_utility.String("自定义数据说明"),
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
GoodsTag: wxpay_utility.String("WXG"),
SupportFapiao: wxpay_utility.Bool(false),
Amount: &CommonAmountInfo{
Total: wxpay_utility.Int64(100),
Currency: wxpay_utility.String("CNY"),
},
Detail: &CouponInfo{
CostPrice: wxpay_utility.Int64(608800),
InvoiceId: wxpay_utility.String("微信123"),
GoodsDetail: []GoodsDetail{GoodsDetail{
MerchantGoodsId: wxpay_utility.String("1246464644"),
WechatpayGoodsId: wxpay_utility.String("1001"),
GoodsName: wxpay_utility.String("iPhoneX 256G"),
Quantity: wxpay_utility.Int64(1),
UnitPrice: wxpay_utility.Int64(528800),
}},
},
SceneInfo: &CommonSceneInfo{
PayerClientIp: wxpay_utility.String("14.23.150.211"),
DeviceId: wxpay_utility.String("013467007045764"),
StoreInfo: &StoreInfo{
Id: wxpay_utility.String("0001"),
Name: wxpay_utility.String("腾讯大厦分店"),
AreaCode: wxpay_utility.String("440305"),
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
},
},
SettleInfo: &SettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
}
response, err := AppPrepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// AppPrepay App下单
func AppPrepay(config *wxpay_utility.MchConfig, request *CommonPrepayRequest) (response *DirectApiv3AppPrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/pay/transactions/app"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &DirectApiv3AppPrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type CommonPrepayRequest struct {
Appid *string `json:"appid,omitempty"`
Mchid *string `json:"mchid,omitempty"`
Description *string `json:"description,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
Attach *string `json:"attach,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SupportFapiao *bool `json:"support_fapiao,omitempty"`
Amount *CommonAmountInfo `json:"amount,omitempty"`
Detail *CouponInfo `json:"detail,omitempty"`
SceneInfo *CommonSceneInfo `json:"scene_info,omitempty"`
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
}
type DirectApiv3AppPrepayResponse struct {
PrepayId *string `json:"prepay_id,omitempty"`
}
type CommonAmountInfo struct {
Total *int64 `json:"total,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type CouponInfo struct {
CostPrice *int64 `json:"cost_price,omitempty"`
InvoiceId *string `json:"invoice_id,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type CommonSceneInfo struct {
PayerClientIp *string `json:"payer_client_ip,omitempty"`
DeviceId *string `json:"device_id,omitempty"`
StoreInfo *StoreInfo `json:"store_info,omitempty"`
}
type SettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
}
type StoreInfo struct {
Id *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
AreaCode *string `json:"area_code,omitempty"`
Address *string `json:"address,omitempty"`
}

View File

@@ -0,0 +1,212 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &DirectApiv3H5PrepayRequest{
Appid: wxpay_utility.String("wxd678efh567hg6787"),
Mchid: wxpay_utility.String("1230000109"),
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
TimeExpire: wxpay_utility.Time(time.Now()),
Attach: wxpay_utility.String("自定义数据说明"),
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
GoodsTag: wxpay_utility.String("WXG"),
SupportFapiao: wxpay_utility.Bool(false),
Amount: &CommonAmountInfo{
Total: wxpay_utility.Int64(100),
Currency: wxpay_utility.String("CNY"),
},
Detail: &CouponInfo{
CostPrice: wxpay_utility.Int64(608800),
InvoiceId: wxpay_utility.String("微信123"),
GoodsDetail: []GoodsDetail{GoodsDetail{
MerchantGoodsId: wxpay_utility.String("1246464644"),
WechatpayGoodsId: wxpay_utility.String("1001"),
GoodsName: wxpay_utility.String("iPhoneX 256G"),
Quantity: wxpay_utility.Int64(1),
UnitPrice: wxpay_utility.Int64(528800),
}},
},
SceneInfo: &H5ReqSceneInfo{
PayerClientIp: wxpay_utility.String("14.23.150.211"),
DeviceId: wxpay_utility.String("013467007045764"),
StoreInfo: &StoreInfo{
Id: wxpay_utility.String("0001"),
Name: wxpay_utility.String("腾讯大厦分店"),
AreaCode: wxpay_utility.String("440305"),
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
},
H5Info: &H5Info{
Type: wxpay_utility.String("iOS"),
AppName: wxpay_utility.String("王者荣耀"),
AppUrl: wxpay_utility.String("https://pay.qq.com"),
BundleId: wxpay_utility.String("com.tencent.wzryiOS"),
PackageName: wxpay_utility.String("com.tencent.tmgp.sgame"),
},
},
SettleInfo: &SettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
}
response, err := H5Prepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// H5Prepay H5下单
func H5Prepay(config *wxpay_utility.MchConfig, request *DirectApiv3H5PrepayRequest) (response *DirectApiv3H5PrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/pay/transactions/h5"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &DirectApiv3H5PrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type DirectApiv3H5PrepayRequest struct {
Appid *string `json:"appid,omitempty"`
Mchid *string `json:"mchid,omitempty"`
Description *string `json:"description,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
Attach *string `json:"attach,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SupportFapiao *bool `json:"support_fapiao,omitempty"`
Amount *CommonAmountInfo `json:"amount,omitempty"`
Detail *CouponInfo `json:"detail,omitempty"`
SceneInfo *H5ReqSceneInfo `json:"scene_info,omitempty"`
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
}
type DirectApiv3H5PrepayResponse struct {
H5Url *string `json:"h5_url,omitempty"`
}
type CommonAmountInfo struct {
Total *int64 `json:"total,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type CouponInfo struct {
CostPrice *int64 `json:"cost_price,omitempty"`
InvoiceId *string `json:"invoice_id,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type H5ReqSceneInfo struct {
PayerClientIp *string `json:"payer_client_ip,omitempty"`
DeviceId *string `json:"device_id,omitempty"`
StoreInfo *StoreInfo `json:"store_info,omitempty"`
H5Info *H5Info `json:"h5_info,omitempty"`
}
type SettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
}
type StoreInfo struct {
Id *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
AreaCode *string `json:"area_code,omitempty"`
Address *string `json:"address,omitempty"`
}
type H5Info struct {
Type *string `json:"type,omitempty"`
AppName *string `json:"app_name,omitempty"`
AppUrl *string `json:"app_url,omitempty"`
BundleId *string `json:"bundle_id,omitempty"`
PackageName *string `json:"package_name,omitempty"`
}

View File

@@ -0,0 +1,196 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &CommonPrepayRequest{
Appid: wxpay_utility.String("wxd678efh567hg6787"),
Mchid: wxpay_utility.String("1230000109"),
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
TimeExpire: wxpay_utility.Time(time.Now()),
Attach: wxpay_utility.String("自定义数据说明"),
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
GoodsTag: wxpay_utility.String("WXG"),
SupportFapiao: wxpay_utility.Bool(false),
Amount: &CommonAmountInfo{
Total: wxpay_utility.Int64(100),
Currency: wxpay_utility.String("CNY"),
},
Detail: &CouponInfo{
CostPrice: wxpay_utility.Int64(608800),
InvoiceId: wxpay_utility.String("微信123"),
GoodsDetail: []GoodsDetail{GoodsDetail{
MerchantGoodsId: wxpay_utility.String("1246464644"),
WechatpayGoodsId: wxpay_utility.String("1001"),
GoodsName: wxpay_utility.String("iPhoneX 256G"),
Quantity: wxpay_utility.Int64(1),
UnitPrice: wxpay_utility.Int64(528800),
}},
},
SceneInfo: &CommonSceneInfo{
PayerClientIp: wxpay_utility.String("14.23.150.211"),
DeviceId: wxpay_utility.String("013467007045764"),
StoreInfo: &StoreInfo{
Id: wxpay_utility.String("0001"),
Name: wxpay_utility.String("腾讯大厦分店"),
AreaCode: wxpay_utility.String("440305"),
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
},
},
SettleInfo: &SettleInfo{
ProfitSharing: wxpay_utility.Bool(false),
},
}
response, err := NativePrepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// NativePrepay Native下单
func NativePrepay(config *wxpay_utility.MchConfig, request *CommonPrepayRequest) (response *DirectApiv3DirectNativePrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/pay/transactions/native"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &DirectApiv3DirectNativePrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type CommonPrepayRequest struct {
Appid *string `json:"appid,omitempty"`
Mchid *string `json:"mchid,omitempty"`
Description *string `json:"description,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
Attach *string `json:"attach,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SupportFapiao *bool `json:"support_fapiao,omitempty"`
Amount *CommonAmountInfo `json:"amount,omitempty"`
Detail *CouponInfo `json:"detail,omitempty"`
SceneInfo *CommonSceneInfo `json:"scene_info,omitempty"`
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
}
type DirectApiv3DirectNativePrepayResponse struct {
CodeUrl *string `json:"code_url,omitempty"`
}
type CommonAmountInfo struct {
Total *int64 `json:"total,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type CouponInfo struct {
CostPrice *int64 `json:"cost_price,omitempty"`
InvoiceId *string `json:"invoice_id,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type CommonSceneInfo struct {
PayerClientIp *string `json:"payer_client_ip,omitempty"`
DeviceId *string `json:"device_id,omitempty"`
StoreInfo *StoreInfo `json:"store_info,omitempty"`
}
type SettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
}
type StoreInfo struct {
Id *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
AreaCode *string `json:"area_code,omitempty"`
Address *string `json:"address,omitempty"`
}

View File

@@ -0,0 +1,178 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryByOutTradeNoRequest{
Mchid: wxpay_utility.String("1230000109"),
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
}
response, err := QueryByOutTradeNo(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func QueryByOutTradeNo(config *wxpay_utility.MchConfig, request *QueryByOutTradeNoRequest) (response *DirectApiv3QueryResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/pay/transactions/out-trade-no/{out_trade_no}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_trade_no}", url.PathEscape(*request.OutTradeNo), -1)
query := reqUrl.Query()
if request.Mchid != nil {
query.Add("mchid", *request.Mchid)
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &DirectApiv3QueryResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type QueryByOutTradeNoRequest struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
}
func (o *QueryByOutTradeNoRequest) MarshalJSON() ([]byte, error) {
type Alias QueryByOutTradeNoRequest
a := &struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
Mchid: nil,
OutTradeNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type DirectApiv3QueryResponse struct {
Appid *string `json:"appid,omitempty"`
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
TradeType *string `json:"trade_type,omitempty"`
TradeState *string `json:"trade_state,omitempty"`
TradeStateDesc *string `json:"trade_state_desc,omitempty"`
BankType *string `json:"bank_type,omitempty"`
Attach *string `json:"attach,omitempty"`
SuccessTime *string `json:"success_time,omitempty"`
Payer *CommRespPayerInfo `json:"payer,omitempty"`
Amount *CommRespAmountInfo `json:"amount,omitempty"`
SceneInfo *CommRespSceneInfo `json:"scene_info,omitempty"`
PromotionDetail []PromotionDetail `json:"promotion_detail,omitempty"`
}
type CommRespPayerInfo struct {
Openid *string `json:"openid,omitempty"`
}
type CommRespAmountInfo struct {
Total *int64 `json:"total,omitempty"`
PayerTotal *int64 `json:"payer_total,omitempty"`
Currency *string `json:"currency,omitempty"`
PayerCurrency *string `json:"payer_currency,omitempty"`
}
type CommRespSceneInfo struct {
DeviceId *string `json:"device_id,omitempty"`
}
type PromotionDetail struct {
CouponId *string `json:"coupon_id,omitempty"`
Name *string `json:"name,omitempty"`
Scope *string `json:"scope,omitempty"`
Type *string `json:"type,omitempty"`
Amount *int64 `json:"amount,omitempty"`
StockId *string `json:"stock_id,omitempty"`
WechatpayContribute *int64 `json:"wechatpay_contribute,omitempty"`
MerchantContribute *int64 `json:"merchant_contribute,omitempty"`
OtherContribute *int64 `json:"other_contribute,omitempty"`
Currency *string `json:"currency,omitempty"`
GoodsDetail []GoodsDetailInPromotion `json:"goods_detail,omitempty"`
}
type GoodsDetailInPromotion struct {
GoodsId *string `json:"goods_id,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
DiscountAmount *int64 `json:"discount_amount,omitempty"`
GoodsRemark *string `json:"goods_remark,omitempty"`
}

View File

@@ -0,0 +1,178 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryByWxTradeNoRequest{
Mchid: wxpay_utility.String("1230000109"),
TransactionId: wxpay_utility.String("1217752501201407033233368018"),
}
response, err := QueryByWxTradeNo(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func QueryByWxTradeNo(config *wxpay_utility.MchConfig, request *QueryByWxTradeNoRequest) (response *DirectApiv3QueryResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/pay/transactions/id/{transaction_id}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{transaction_id}", url.PathEscape(*request.TransactionId), -1)
query := reqUrl.Query()
if request.Mchid != nil {
query.Add("mchid", *request.Mchid)
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &DirectApiv3QueryResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type QueryByWxTradeNoRequest struct {
Mchid *string `json:"mchid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
}
func (o *QueryByWxTradeNoRequest) MarshalJSON() ([]byte, error) {
type Alias QueryByWxTradeNoRequest
a := &struct {
Mchid *string `json:"mchid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
Mchid: nil,
TransactionId: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type DirectApiv3QueryResponse struct {
Appid *string `json:"appid,omitempty"`
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
TradeType *string `json:"trade_type,omitempty"`
TradeState *string `json:"trade_state,omitempty"`
TradeStateDesc *string `json:"trade_state_desc,omitempty"`
BankType *string `json:"bank_type,omitempty"`
Attach *string `json:"attach,omitempty"`
SuccessTime *string `json:"success_time,omitempty"`
Payer *CommRespPayerInfo `json:"payer,omitempty"`
Amount *CommRespAmountInfo `json:"amount,omitempty"`
SceneInfo *CommRespSceneInfo `json:"scene_info,omitempty"`
PromotionDetail []PromotionDetail `json:"promotion_detail,omitempty"`
}
type CommRespPayerInfo struct {
Openid *string `json:"openid,omitempty"`
}
type CommRespAmountInfo struct {
Total *int64 `json:"total,omitempty"`
PayerTotal *int64 `json:"payer_total,omitempty"`
Currency *string `json:"currency,omitempty"`
PayerCurrency *string `json:"payer_currency,omitempty"`
}
type CommRespSceneInfo struct {
DeviceId *string `json:"device_id,omitempty"`
}
type PromotionDetail struct {
CouponId *string `json:"coupon_id,omitempty"`
Name *string `json:"name,omitempty"`
Scope *string `json:"scope,omitempty"`
Type *string `json:"type,omitempty"`
Amount *int64 `json:"amount,omitempty"`
StockId *string `json:"stock_id,omitempty"`
WechatpayContribute *int64 `json:"wechatpay_contribute,omitempty"`
MerchantContribute *int64 `json:"merchant_contribute,omitempty"`
OtherContribute *int64 `json:"other_contribute,omitempty"`
Currency *string `json:"currency,omitempty"`
GoodsDetail []GoodsDetailInPromotion `json:"goods_detail,omitempty"`
}
type GoodsDetailInPromotion struct {
GoodsId *string `json:"goods_id,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
DiscountAmount *int64 `json:"discount_amount,omitempty"`
GoodsRemark *string `json:"goods_remark,omitempty"`
}

View File

@@ -0,0 +1,129 @@
// 关闭订单
//
// 未支付状态的订单,可在无需支付时调用此接口关闭订单。常见关单情况包括:
// 1. 用户在商户系统提交取消订单请求,商户需执行关单操作。
// 2. 订单超时未支付超出商户系统设定的可支付时间或下单时的time_expire支付截止时间商户需进行关单处理。
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &CloseOrderRequest{
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
Mchid: wxpay_utility.String("1230000109"),
}
err = CloseOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Println("请求成功")
}
// CloseOrder 关闭订单
//
// 未支付状态的订单,可在无需支付时调用此接口关闭订单。常见关单情况包括:
// 1. 用户在商户系统提交取消订单请求,商户需执行关单操作。
// 2. 订单超时未支付超出商户系统设定的可支付时间或下单时的time_expire支付截止时间商户需进行关单处理。
func CloseOrder(config *wxpay_utility.MchConfig, request *CloseOrderRequest) (err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/pay/transactions/out-trade-no/{out_trade_no}/close"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_trade_no}", url.PathEscape(*request.OutTradeNo), -1)
reqBody, err := json.Marshal(request)
if err != nil {
return err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return err
}
return nil
} else {
return wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type CloseOrderRequest struct {
Mchid *string `json:"mchid,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
}
func (o *CloseOrderRequest) MarshalJSON() ([]byte, error) {
type Alias CloseOrderRequest
a := &struct {
OutTradeNo *string `json:"out_trade_no,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
OutTradeNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}

View File

@@ -0,0 +1,269 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &CreateAbnormalRefundRequest{
RefundId: wxpay_utility.String("50000000382019052709732678859"),
OutRefundNo: wxpay_utility.String("1217752501201407033233368018"),
Type: ABNORMALRECEIVETYPE_MERCHANT_BANK_CARD.Ptr(),
BankType: wxpay_utility.String("ICBC_DEBIT"),
BankAccount: wxpay_utility.String("d+xT+MQCvrLHUVDWv/8MR/dB7TkXLVfSrUxMPZy6jWWYzpRrEEaYQE8ZRGYoeorwC+w=="), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
RealName: wxpay_utility.String("UPgQcZSdq3zOayJwZ5XLrHY2dZU1W2Cd"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
}
response, err := CreateAbnormalRefund(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// CreateAbnormalRefund 发起异常退款
func CreateAbnormalRefund(config *wxpay_utility.MchConfig, request *CreateAbnormalRefundRequest) (response *Refund, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{refund_id}", url.PathEscape(*request.RefundId), -1)
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &Refund{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type CreateAbnormalRefundRequest struct {
RefundId *string `json:"refund_id,omitempty"`
OutRefundNo *string `json:"out_refund_no,omitempty"`
Type *AbnormalReceiveType `json:"type,omitempty"`
BankType *string `json:"bank_type,omitempty"`
BankAccount *string `json:"bank_account,omitempty"`
RealName *string `json:"real_name,omitempty"`
}
func (o *CreateAbnormalRefundRequest) MarshalJSON() ([]byte, error) {
type Alias CreateAbnormalRefundRequest
a := &struct {
RefundId *string `json:"refund_id,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
RefundId: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type Refund struct {
RefundId *string `json:"refund_id,omitempty"`
OutRefundNo *string `json:"out_refund_no,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
Channel *Channel `json:"channel,omitempty"`
UserReceivedAccount *string `json:"user_received_account,omitempty"`
SuccessTime *time.Time `json:"success_time,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
Status *Status `json:"status,omitempty"`
FundsAccount *FundsAccount `json:"funds_account,omitempty"`
Amount *Amount `json:"amount,omitempty"`
PromotionDetail []Promotion `json:"promotion_detail,omitempty"`
}
type AbnormalReceiveType string
func (e AbnormalReceiveType) Ptr() *AbnormalReceiveType {
return &e
}
const (
ABNORMALRECEIVETYPE_USER_BANK_CARD AbnormalReceiveType = "USER_BANK_CARD"
ABNORMALRECEIVETYPE_MERCHANT_BANK_CARD AbnormalReceiveType = "MERCHANT_BANK_CARD"
)
type Channel string
func (e Channel) Ptr() *Channel {
return &e
}
const (
CHANNEL_ORIGINAL Channel = "ORIGINAL"
CHANNEL_BALANCE Channel = "BALANCE"
CHANNEL_OTHER_BALANCE Channel = "OTHER_BALANCE"
CHANNEL_OTHER_BANKCARD Channel = "OTHER_BANKCARD"
)
type Status string
func (e Status) Ptr() *Status {
return &e
}
const (
STATUS_SUCCESS Status = "SUCCESS"
STATUS_CLOSED Status = "CLOSED"
STATUS_PROCESSING Status = "PROCESSING"
STATUS_ABNORMAL Status = "ABNORMAL"
)
type FundsAccount string
func (e FundsAccount) Ptr() *FundsAccount {
return &e
}
const (
FUNDSACCOUNT_UNSETTLED FundsAccount = "UNSETTLED"
FUNDSACCOUNT_AVAILABLE FundsAccount = "AVAILABLE"
FUNDSACCOUNT_UNAVAILABLE FundsAccount = "UNAVAILABLE"
FUNDSACCOUNT_OPERATION FundsAccount = "OPERATION"
FUNDSACCOUNT_BASIC FundsAccount = "BASIC"
FUNDSACCOUNT_ECNY_BASIC FundsAccount = "ECNY_BASIC"
)
type Amount struct {
Total *int64 `json:"total,omitempty"`
Refund *int64 `json:"refund,omitempty"`
From []FundsFromItem `json:"from,omitempty"`
PayerTotal *int64 `json:"payer_total,omitempty"`
PayerRefund *int64 `json:"payer_refund,omitempty"`
SettlementRefund *int64 `json:"settlement_refund,omitempty"`
SettlementTotal *int64 `json:"settlement_total,omitempty"`
DiscountRefund *int64 `json:"discount_refund,omitempty"`
Currency *string `json:"currency,omitempty"`
RefundFee *int64 `json:"refund_fee,omitempty"`
}
type Promotion struct {
PromotionId *string `json:"promotion_id,omitempty"`
Scope *PromotionScope `json:"scope,omitempty"`
Type *PromotionType `json:"type,omitempty"`
Amount *int64 `json:"amount,omitempty"`
RefundAmount *int64 `json:"refund_amount,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type FundsFromItem struct {
Account *Account `json:"account,omitempty"`
Amount *int64 `json:"amount,omitempty"`
}
type PromotionScope string
func (e PromotionScope) Ptr() *PromotionScope {
return &e
}
const (
PROMOTIONSCOPE_GLOBAL PromotionScope = "GLOBAL"
PROMOTIONSCOPE_SINGLE PromotionScope = "SINGLE"
)
type PromotionType string
func (e PromotionType) Ptr() *PromotionType {
return &e
}
const (
PROMOTIONTYPE_CASH PromotionType = "CASH"
PROMOTIONTYPE_NOCASH PromotionType = "NOCASH"
)
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
RefundAmount *int64 `json:"refund_amount,omitempty"`
RefundQuantity *int64 `json:"refund_quantity,omitempty"`
}
type Account string
func (e Account) Ptr() *Account {
return &e
}
const (
ACCOUNT_AVAILABLE Account = "AVAILABLE"
ACCOUNT_UNAVAILABLE Account = "UNAVAILABLE"
)

View File

@@ -0,0 +1,300 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
// 退款申请
//
// 支付成功后1年内可通过此接口将款项全部或部分原路退还给用户也可在商户平台手动操作
//
// 关键注意:
// 1. 一笔订单最多50次部分退款重试必须用原 out_refund_no否则会重复退款。
// 2. 接口返回成功仅表示受理成功,实际结果以退款回调通知或查询退款接口为准。
// 3. 原路退还银行卡1-3个工作日到账零钱即时到账。
// 4. 有代金券的订单部分退款时,退给用户 = 退款金额 × (实付 ÷ 总额),四舍五入。
// 5. 有分账的订单,需确保可用余额充足;部分分账未解冻时需先调"完结分账"。
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &CreateRequest{
TransactionId: wxpay_utility.String("1217752501201407033233368018"),
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
OutRefundNo: wxpay_utility.String("1217752501201407033233368018"),
Reason: wxpay_utility.String("商品已售完"),
NotifyUrl: wxpay_utility.String("https://weixin.qq.com"),
FundsAccount: REQFUNDSACCOUNT_AVAILABLE.Ptr(),
Amount: &AmountReq{
Refund: wxpay_utility.Int64(888),
From: []FundsFromItem{FundsFromItem{
Account: ACCOUNT_AVAILABLE.Ptr(),
Amount: wxpay_utility.Int64(444),
}},
Total: wxpay_utility.Int64(888),
Currency: wxpay_utility.String("CNY"),
},
GoodsDetail: []GoodsDetail{GoodsDetail{
MerchantGoodsId: wxpay_utility.String("1217752501201407033233368018"),
WechatpayGoodsId: wxpay_utility.String("1001"),
GoodsName: wxpay_utility.String("iPhone6s 16G"),
UnitPrice: wxpay_utility.Int64(528800),
RefundAmount: wxpay_utility.Int64(528800),
RefundQuantity: wxpay_utility.Int64(1),
}},
}
response, err := CreateRefund(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// CreateRefund 退款申请
//
// 在交易完成后的一年内(以支付成功时间为起点+365天计算若因用户或商户方面导致需进行订单退款
// 商户可通过此接口将支付金额的全部或部分原路退还至用户。
//
// 注意:
// 1. 一笔订单最多支持50次部分退款若需多次部分退款请更换商户退款单号并间隔1分钟后再次调用
// 2. 在申请退款失败后进行重试时,请务必使用原商户退款单号,以避免因重复退款而导致的资金损失。
// 3. 同一商户号下此接口调用成功的频率限制为150QPS而调用失败报错时的频率限制为6QPS。
// 4. 申请退款接口返回成功仅表示退款单已受理成功,具体的退款结果需依据退款结果通知及查询退款的返回信息为准。
// 5. 若一个月前的订单申请退款时返回报错"频率限制1个月之前的订单请降低申请频率再重试",请调整退款时间,再使用原参数进行重试。
func CreateRefund(config *wxpay_utility.MchConfig, request *CreateRequest) (response *Refund, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/refund/domestic/refunds"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &Refund{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type CreateRequest struct {
TransactionId *string `json:"transaction_id,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
OutRefundNo *string `json:"out_refund_no,omitempty"`
Reason *string `json:"reason,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
FundsAccount *ReqFundsAccount `json:"funds_account,omitempty"`
Amount *AmountReq `json:"amount,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type Refund struct {
RefundId *string `json:"refund_id,omitempty"`
OutRefundNo *string `json:"out_refund_no,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
Channel *Channel `json:"channel,omitempty"`
UserReceivedAccount *string `json:"user_received_account,omitempty"`
SuccessTime *time.Time `json:"success_time,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
Status *Status `json:"status,omitempty"`
FundsAccount *FundsAccount `json:"funds_account,omitempty"`
Amount *Amount `json:"amount,omitempty"`
PromotionDetail []Promotion `json:"promotion_detail,omitempty"`
}
type ReqFundsAccount string
func (e ReqFundsAccount) Ptr() *ReqFundsAccount {
return &e
}
const (
REQFUNDSACCOUNT_AVAILABLE ReqFundsAccount = "AVAILABLE"
REQFUNDSACCOUNT_UNSETTLED ReqFundsAccount = "UNSETTLED"
)
type AmountReq struct {
Refund *int64 `json:"refund,omitempty"`
From []FundsFromItem `json:"from,omitempty"`
Total *int64 `json:"total,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
RefundAmount *int64 `json:"refund_amount,omitempty"`
RefundQuantity *int64 `json:"refund_quantity,omitempty"`
}
type Channel string
func (e Channel) Ptr() *Channel {
return &e
}
const (
CHANNEL_ORIGINAL Channel = "ORIGINAL"
CHANNEL_BALANCE Channel = "BALANCE"
CHANNEL_OTHER_BALANCE Channel = "OTHER_BALANCE"
CHANNEL_OTHER_BANKCARD Channel = "OTHER_BANKCARD"
)
type Status string
func (e Status) Ptr() *Status {
return &e
}
const (
STATUS_SUCCESS Status = "SUCCESS"
STATUS_CLOSED Status = "CLOSED"
STATUS_PROCESSING Status = "PROCESSING"
STATUS_ABNORMAL Status = "ABNORMAL"
)
type FundsAccount string
func (e FundsAccount) Ptr() *FundsAccount {
return &e
}
const (
FUNDSACCOUNT_UNSETTLED FundsAccount = "UNSETTLED"
FUNDSACCOUNT_AVAILABLE FundsAccount = "AVAILABLE"
FUNDSACCOUNT_UNAVAILABLE FundsAccount = "UNAVAILABLE"
FUNDSACCOUNT_OPERATION FundsAccount = "OPERATION"
FUNDSACCOUNT_BASIC FundsAccount = "BASIC"
FUNDSACCOUNT_ECNY_BASIC FundsAccount = "ECNY_BASIC"
)
type Amount struct {
Total *int64 `json:"total,omitempty"`
Refund *int64 `json:"refund,omitempty"`
From []FundsFromItem `json:"from,omitempty"`
PayerTotal *int64 `json:"payer_total,omitempty"`
PayerRefund *int64 `json:"payer_refund,omitempty"`
SettlementRefund *int64 `json:"settlement_refund,omitempty"`
SettlementTotal *int64 `json:"settlement_total,omitempty"`
DiscountRefund *int64 `json:"discount_refund,omitempty"`
Currency *string `json:"currency,omitempty"`
RefundFee *int64 `json:"refund_fee,omitempty"`
}
type Promotion struct {
PromotionId *string `json:"promotion_id,omitempty"`
Scope *PromotionScope `json:"scope,omitempty"`
Type *PromotionType `json:"type,omitempty"`
Amount *int64 `json:"amount,omitempty"`
RefundAmount *int64 `json:"refund_amount,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type FundsFromItem struct {
Account *Account `json:"account,omitempty"`
Amount *int64 `json:"amount,omitempty"`
}
type PromotionScope string
func (e PromotionScope) Ptr() *PromotionScope {
return &e
}
const (
PROMOTIONSCOPE_GLOBAL PromotionScope = "GLOBAL"
PROMOTIONSCOPE_SINGLE PromotionScope = "SINGLE"
)
type PromotionType string
func (e PromotionType) Ptr() *PromotionType {
return &e
}
const (
PROMOTIONTYPE_CASH PromotionType = "CASH"
PROMOTIONTYPE_NOCASH PromotionType = "NOCASH"
)
type Account string
func (e Account) Ptr() *Account {
return &e
}
const (
ACCOUNT_AVAILABLE Account = "AVAILABLE"
ACCOUNT_UNAVAILABLE Account = "UNAVAILABLE"
)

View File

@@ -0,0 +1,251 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryByOutRefundNoRequest{
OutRefundNo: wxpay_utility.String("1217752501201407033233368018"),
}
response, err := QueryByOutRefundNo(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
// QueryByOutRefundNo 查询单笔退款(通过商户退款单号)
//
// 提交退款申请后推荐每间隔1分钟调用该接口查询一次退款状态若超过5分钟仍是退款处理中状态
// 建议开始逐步衰减查询频率(比如之后间隔5分钟、10分钟、20分钟、30分钟……查询一次)。
//
// 退款有一定延时零钱支付的订单退款一般5分钟内到账银行卡支付的订单退款一般1-3个工作日到账。
//
// 同一商户号查询退款频率限制为300qps如返回FREQUENCY_LIMITED频率限制报错可间隔1分钟再重试查询。
func QueryByOutRefundNo(config *wxpay_utility.MchConfig, request *QueryByOutRefundNoRequest) (response *Refund, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/refund/domestic/refunds/{out_refund_no}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_refund_no}", url.PathEscape(*request.OutRefundNo), -1)
query := reqUrl.Query()
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &Refund{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type QueryByOutRefundNoRequest struct {
OutRefundNo *string `json:"out_refund_no,omitempty"`
}
func (o *QueryByOutRefundNoRequest) MarshalJSON() ([]byte, error) {
type Alias QueryByOutRefundNoRequest
a := &struct {
OutRefundNo *string `json:"out_refund_no,omitempty"`
*Alias
}{
// 序列化时移除非 Body 字段
OutRefundNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type Refund struct {
RefundId *string `json:"refund_id,omitempty"`
OutRefundNo *string `json:"out_refund_no,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
Channel *Channel `json:"channel,omitempty"`
UserReceivedAccount *string `json:"user_received_account,omitempty"`
SuccessTime *time.Time `json:"success_time,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
Status *Status `json:"status,omitempty"`
FundsAccount *FundsAccount `json:"funds_account,omitempty"`
Amount *Amount `json:"amount,omitempty"`
PromotionDetail []Promotion `json:"promotion_detail,omitempty"`
}
type Channel string
func (e Channel) Ptr() *Channel {
return &e
}
const (
CHANNEL_ORIGINAL Channel = "ORIGINAL"
CHANNEL_BALANCE Channel = "BALANCE"
CHANNEL_OTHER_BALANCE Channel = "OTHER_BALANCE"
CHANNEL_OTHER_BANKCARD Channel = "OTHER_BANKCARD"
)
type Status string
func (e Status) Ptr() *Status {
return &e
}
const (
STATUS_SUCCESS Status = "SUCCESS"
STATUS_CLOSED Status = "CLOSED"
STATUS_PROCESSING Status = "PROCESSING"
STATUS_ABNORMAL Status = "ABNORMAL"
)
type FundsAccount string
func (e FundsAccount) Ptr() *FundsAccount {
return &e
}
const (
FUNDSACCOUNT_UNSETTLED FundsAccount = "UNSETTLED"
FUNDSACCOUNT_AVAILABLE FundsAccount = "AVAILABLE"
FUNDSACCOUNT_UNAVAILABLE FundsAccount = "UNAVAILABLE"
FUNDSACCOUNT_OPERATION FundsAccount = "OPERATION"
FUNDSACCOUNT_BASIC FundsAccount = "BASIC"
FUNDSACCOUNT_ECNY_BASIC FundsAccount = "ECNY_BASIC"
)
type Amount struct {
Total *int64 `json:"total,omitempty"`
Refund *int64 `json:"refund,omitempty"`
From []FundsFromItem `json:"from,omitempty"`
PayerTotal *int64 `json:"payer_total,omitempty"`
PayerRefund *int64 `json:"payer_refund,omitempty"`
SettlementRefund *int64 `json:"settlement_refund,omitempty"`
SettlementTotal *int64 `json:"settlement_total,omitempty"`
DiscountRefund *int64 `json:"discount_refund,omitempty"`
Currency *string `json:"currency,omitempty"`
RefundFee *int64 `json:"refund_fee,omitempty"`
}
type Promotion struct {
PromotionId *string `json:"promotion_id,omitempty"`
Scope *PromotionScope `json:"scope,omitempty"`
Type *PromotionType `json:"type,omitempty"`
Amount *int64 `json:"amount,omitempty"`
RefundAmount *int64 `json:"refund_amount,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type FundsFromItem struct {
Account *Account `json:"account,omitempty"`
Amount *int64 `json:"amount,omitempty"`
}
type PromotionScope string
func (e PromotionScope) Ptr() *PromotionScope {
return &e
}
const (
PROMOTIONSCOPE_GLOBAL PromotionScope = "GLOBAL"
PROMOTIONSCOPE_SINGLE PromotionScope = "SINGLE"
)
type PromotionType string
func (e PromotionType) Ptr() *PromotionType {
return &e
}
const (
PROMOTIONTYPE_CASH PromotionType = "CASH"
PROMOTIONTYPE_NOCASH PromotionType = "NOCASH"
)
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
RefundAmount *int64 `json:"refund_amount,omitempty"`
RefundQuantity *int64 `json:"refund_quantity,omitempty"`
}
type Account string
func (e Account) Ptr() *Account {
return &e
}
const (
ACCOUNT_AVAILABLE Account = "AVAILABLE"
ACCOUNT_UNAVAILABLE Account = "UNAVAILABLE"
)

View File

@@ -0,0 +1,39 @@
# JSAPI调起支付
## 说明
商户通过JSAPI/小程序下单接口获取到发起支付的必要参数 `prepay_id`再通过微信浏览器内置对象方法WeixinJSBridge调起微信支付收银台。
## 示例代码
```javascript
function onBridgeReady() {
WeixinJSBridge.invoke('getBrandWCPayRequest', {
"appId": "wx2421b1c4370ec43b", //公众号ID由商户传入
"timeStamp": "1395712654", //时间戳自1970年以来的秒数
"nonceStr": "e61463f8efa94090b1f366cccfbbb444", //随机串
"package": "prepay_id=wx21201855730335ac86f8c43d1889123400",
"signType": "RSA", //微信签名方式
"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg=="
},
function(res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 使用以上方式判断前端返回,微信团队郑重提示:
// res.err_msg将在用户支付成功后返回ok但并不保证它绝对可靠
// 商户需进一步调用后端查单确认支付结果。
}
});
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
```
> **重要**`res.err_msg` 将在用户支付成功后返回 ok但并不保证它绝对可靠商户需进一步调用后端查单确认支付结果。

View File

@@ -0,0 +1,254 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* JSAPI下单
*/
public class JsapiPrepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/pay/transactions/jsapi";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
JsapiPrepay client = new JsapiPrepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
DirectAPIv3JsapiPrepayRequest request = new DirectAPIv3JsapiPrepayRequest();
request.appid = "wxd678efh567hg6787";
request.mchid = "1230000109";
request.description = "Image形象店-深圳腾大-QQ公仔";
request.outTradeNo = "1217752501201407033233368018";
request.timeExpire = "2018-06-08T10:34:56+08:00";
request.attach = "自定义数据说明";
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
request.goodsTag = "WXG";
request.supportFapiao = false;
request.amount = new CommonAmountInfo();
request.amount.total = 100L;
request.amount.currency = "CNY";
request.payer = new JsapiReqPayerInfo();
request.payer.openid = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o";
request.detail = new CouponInfo();
request.detail.costPrice = 608800L;
request.detail.invoiceId = "微信123";
request.detail.goodsDetail = new ArrayList<>();
{
GoodsDetail goodsDetailItem = new GoodsDetail();
goodsDetailItem.merchantGoodsId = "1246464644";
goodsDetailItem.wechatpayGoodsId = "1001";
goodsDetailItem.goodsName = "iPhoneX 256G";
goodsDetailItem.quantity = 1L;
goodsDetailItem.unitPrice = 528800L;
request.detail.goodsDetail.add(goodsDetailItem);
};
request.sceneInfo = new CommonSceneInfo();
request.sceneInfo.payerClientIp = "14.23.150.211";
request.sceneInfo.deviceId = "013467007045764";
request.sceneInfo.storeInfo = new StoreInfo();
request.sceneInfo.storeInfo.id = "0001";
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
request.sceneInfo.storeInfo.areaCode = "440305";
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
request.settleInfo = new SettleInfo();
request.settleInfo.profitSharing = false;
try {
DirectAPIv3JsapiPrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public DirectAPIv3JsapiPrepayResponse run(DirectAPIv3JsapiPrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public JsapiPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class DirectAPIv3JsapiPrepayRequest {
@SerializedName("appid")
public String appid;
@SerializedName("mchid")
public String mchid;
@SerializedName("description")
public String description;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("attach")
public String attach;
@SerializedName("notify_url")
public String notifyUrl;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("support_fapiao")
public Boolean supportFapiao;
@SerializedName("amount")
public CommonAmountInfo amount;
@SerializedName("payer")
public JsapiReqPayerInfo payer;
@SerializedName("detail")
public CouponInfo detail;
@SerializedName("scene_info")
public CommonSceneInfo sceneInfo;
@SerializedName("settle_info")
public SettleInfo settleInfo;
}
public static class DirectAPIv3JsapiPrepayResponse {
@SerializedName("prepay_id")
public String prepayId;
}
public static class CommonAmountInfo {
@SerializedName("total")
public Long total;
@SerializedName("currency")
public String currency;
}
public static class JsapiReqPayerInfo {
@SerializedName("openid")
public String openid;
}
public static class CouponInfo {
@SerializedName("cost_price")
public Long costPrice;
@SerializedName("invoice_id")
public String invoiceId;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class CommonSceneInfo {
@SerializedName("payer_client_ip")
public String payerClientIp;
@SerializedName("device_id")
public String deviceId;
@SerializedName("store_info")
public StoreInfo storeInfo;
}
public static class SettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
public static class GoodsDetail {
@SerializedName("merchant_goods_id")
public String merchantGoodsId;
@SerializedName("wechatpay_goods_id")
public String wechatpayGoodsId;
@SerializedName("goods_name")
public String goodsName;
@SerializedName("quantity")
public Long quantity;
@SerializedName("unit_price")
public Long unitPrice;
}
public static class StoreInfo {
@SerializedName("id")
public String id;
@SerializedName("name")
public String name;
@SerializedName("area_code")
public String areaCode;
@SerializedName("address")
public String address;
}
}

View File

@@ -0,0 +1,84 @@
# 退款结果回调通知
> 参考官方文档https://pay.weixin.qq.com/doc/v3/merchant/4012791865
## 回调描述
用户支付完成后,商户可根据需求发起退款。当退款单状态发生变更时(变更为退款成功/退款关闭/退款异常微信支付会通过POST的请求方式向商户预先设置的退款回调地址申请退款传入的 notify_url发送回调通知让商户知晓退款单的处理结果。
> **注意**商户侧对微信支付回调IP有防火墙策略限制的需要对微信回调IP段开通白名单详情参考回调处理逻辑注意事项。
## 回调报文格式
```json
{
"id": "EV-2018022511223320873",
"create_time": "2018-06-08T10:34:56+08:00",
"resource_type": "encrypt-resource",
"event_type": "REFUND.SUCCESS",
"summary": "退款成功",
"resource": {
"algorithm": "AEAD_AES_256_GCM",
"original_type": "refund",
"ciphertext": "...",
"nonce": "...",
"associated_data": ""
}
}
```
## 关键字段说明
| 字段 | 说明 |
|------|------|
| `event_type` | 退款事件类型:`REFUND.SUCCESS`(退款成功)/ `REFUND.ABNORMAL`(退款异常)/ `REFUND.CLOSED`(退款关闭) |
| `resource.original_type` | 固定为 `refund` |
| `resource.algorithm` | 加密算法,固定为 `AEAD_AES_256_GCM` |
| `resource.ciphertext` | 密文需使用商户APIv3密钥解密后得到退款详情 |
| `resource.associated_data` | 附加数据,解密时作为 AAD 参数 |
| `resource.nonce` | 随机串,解密时作为 Nonce 参数 |
## 解密后的退款详情resource.ciphertext 解密结果)
```json
{
"mchid": "1900000100",
"transaction_id": "1008450740201411110005820873",
"out_trade_no": "20150806125346",
"refund_id": "50200207182018070300011301001",
"out_refund_no": "7752501201407033233368018",
"refund_status": "SUCCESS",
"success_time": "2018-06-08T10:34:56+08:00",
"user_received_account": "招商银行信用卡0403",
"amount": {
"total": 999,
"refund": 999,
"payer_total": 999,
"payer_refund": 999
}
}
```
| 字段 | 说明 |
|------|------|
| `refund_status` | 退款状态:`SUCCESS`(成功)/ `CLOSED`(关闭)/ `PROCESSING`(处理中)/ `ABNORMAL`(异常) |
| `user_received_account` | 退款入账账户,如"招商银行信用卡0403"、"支付用户零钱"等 |
| `amount.total` | 原订单金额(分) |
| `amount.refund` | 退款金额(分) |
| `amount.payer_total` | 用户实际支付金额(分) |
| `amount.payer_refund` | 用户实际收到的退款金额(分) |
## 处理要求
1. 接收到回调后,先验证请求签名(使用微信支付公钥验签)
2. 使用商户APIv3密钥 + AEAD_AES_256_GCM 解密 `resource.ciphertext` 得到退款明文
3. 验签通过:返回 HTTP 200 或 204无需返回应答报文
4. 验签不通过:返回 HTTP 4XX/5XX + `{"code": "FAIL", "message": "失败"}`
5. 应答后再异步处理业务逻辑,避免超时
## 重试机制
若商户应答失败或超时5秒微信支付按以下频次重试
15s / 15s / 30s / 3m / 10m / 20m / 30m / 30m / 30m / 60m / 3h / 3h / 3h / 6h / 6h最多15次
> **重要**:商户系统不能仅依赖回调通知获取结果,需结合查询退款接口使用,避免遗漏或延迟问题。收到重复通知时请做好幂等处理并持续应答 200。

View File

@@ -0,0 +1,154 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 申请交易账单API
*
* 关键注意:
* 1. 次日10点后拉取API仅支持3个月内单日账单更早的需在商户平台下载。
* 2. 返回的是下载链接download_url需二次请求下载gzip压缩CSV
* 3. 账单金额单位为"元"与下单API的"分"不同,对账时注意转换。
*/
public class GetTradeBill {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/bill/tradebill";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
GetTradeBill client = new GetTradeBill(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
GetTradeBillRequest request = new GetTradeBillRequest();
request.billDate = "2019-06-11";
request.billType = BillType.ALL;
request.tarType = TarType.GZIP;
try {
QueryBillEntity response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public QueryBillEntity run(GetTradeBillRequest request) {
String uri = PATH;
Map<String, Object> args = new HashMap<>();
args.put("bill_date", request.billDate);
args.put("bill_type", request.billType);
args.put("tar_type", request.tarType);
String queryString = WXPayUtility.urlEncode(args);
if (!queryString.isEmpty()) {
uri = uri + "?" + queryString;
}
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, QueryBillEntity.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public GetTradeBill(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class GetTradeBillRequest {
@SerializedName("bill_date")
@Expose(serialize = false)
public String billDate;
@SerializedName("bill_type")
@Expose(serialize = false)
public BillType billType;
@SerializedName("tar_type")
@Expose(serialize = false)
public TarType tarType;
}
public static class QueryBillEntity {
@SerializedName("hash_type")
public HashType hashType;
@SerializedName("hash_value")
public String hashValue;
@SerializedName("download_url")
public String downloadUrl;
}
public enum BillType {
@SerializedName("ALL")
ALL,
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("REFUND")
REFUND
}
public enum TarType {
@SerializedName("GZIP")
GZIP
}
public enum HashType {
@SerializedName("SHA1")
SHA1
}
}

View File

@@ -0,0 +1,149 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 申请资金账单API
*/
public class GetFundFlowBill {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/bill/fundflowbill";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
GetFundFlowBill client = new GetFundFlowBill(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
GetFundFlowBillRequest request = new GetFundFlowBillRequest();
request.billDate = "2019-06-11";
request.accountType = FundFlowBillAccountType.BASIC;
request.tarType = TarType.GZIP;
try {
QueryBillEntity response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public QueryBillEntity run(GetFundFlowBillRequest request) {
String uri = PATH;
Map<String, Object> args = new HashMap<>();
args.put("bill_date", request.billDate);
args.put("account_type", request.accountType);
args.put("tar_type", request.tarType);
String queryString = WXPayUtility.urlEncode(args);
if (!queryString.isEmpty()) {
uri = uri + "?" + queryString;
}
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, QueryBillEntity.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public GetFundFlowBill(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class GetFundFlowBillRequest {
@SerializedName("bill_date")
@Expose(serialize = false)
public String billDate;
@SerializedName("account_type")
@Expose(serialize = false)
public FundFlowBillAccountType accountType;
@SerializedName("tar_type")
@Expose(serialize = false)
public TarType tarType;
}
public static class QueryBillEntity {
@SerializedName("hash_type")
public HashType hashType;
@SerializedName("hash_value")
public String hashValue;
@SerializedName("download_url")
public String downloadUrl;
}
public enum FundFlowBillAccountType {
@SerializedName("BASIC")
BASIC,
@SerializedName("OPERATION")
OPERATION,
@SerializedName("FEES")
FEES
}
public enum TarType {
@SerializedName("GZIP")
GZIP
}
public enum HashType {
@SerializedName("SHA1")
SHA1
}
}

View File

@@ -0,0 +1,167 @@
package com.java.demo;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import com.java.utils.WXPayUtility; // 引用微信支付工具库 参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okhttp3.Response;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.zip.GZIPInputStream;
/**
* 下载账单
*
* 当商户调用申请交易账单/申请资金账单接口获取到下载账单链接download_url后
* 需按照V3接口规则生成签名然后请求下载账单链接download_url获取对应的账单文件。
*/
public class DownloadBill {
private static String METHOD = "GET";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
DownloadBill client = new DownloadBill(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
DownloadBillRequest request = new DownloadBillRequest();
request.downloadUrl = "https://api.mch.weixin.qq.com/v3/billdownload/file?token=xxx";
request.localFilePath = "downloaded_bill.csv";
request.expectedHashType = HashType.SHA1;
request.expectedHashValue = "79bb0f45fc4c42234a918000b2668d689e2bde04";
request.tarType = TarType.GZIP;
try {
client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println("File downloaded successfully! Local file path: " + request.localFilePath);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public void run(DownloadBillRequest request) {
Request.Builder reqBuilder = new Request.Builder().url(request.downloadUrl);
String uri = getPathQueryFromUrl(request.downloadUrl);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization",
WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
if (httpResponse.code() < 200 || httpResponse.code() > 300) {
String respBody = WXPayUtility.extractBody(httpResponse);
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
// 2XX 成功,流式下载文件
ResponseBody body = httpResponse.body();
if (body == null) {
throw new IOException("Response body is null");
}
// 读取流
try (InputStream inputStream = (request.tarType == DownloadBill.TarType.GZIP)
? new GZIPInputStream(body.byteStream())
: body.byteStream();
FileOutputStream outputStream = new FileOutputStream(request.localFilePath)) {
byte[] buffer = new byte[8096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
// 下载成功后校验文件SHA1
if (request.expectedHashType == HashType.SHA1) {
String sha1 = DigestUtils.sha1Hex(new FileInputStream(request.localFilePath));
if (!sha1.equals(request.expectedHashValue)) {
throw new IOException("SHA1 checksum mismatch");
}
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private String getPathQueryFromUrl(String url) {
try {
URI uri = new URI(url);
String path = uri.getRawPath(); // /v3/billdownload/file
String query = uri.getRawQuery(); // token=xxx&tartype=gzip
return (query == null || query.isEmpty()) ? path : path + "?" + query;
} catch (URISyntaxException e) {
e.printStackTrace();
return "";
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public DownloadBill(String mchid, String certificateSerialNo, String privateKeyFilePath,
String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public enum HashType {
@SerializedName("SHA1")
SHA1
}
public static class DownloadBillRequest {
@SerializedName("download_url")
@Expose(serialize = false)
public String downloadUrl;
@SerializedName("local_file_path")
@Expose(serialize = false)
public String localFilePath;
@SerializedName("expected_hash_type")
@Expose(serialize = false)
public HashType expectedHashType;
@SerializedName("expected_hash_value")
@Expose(serialize = false)
public String expectedHashValue;
@SerializedName("tar_type")
@Expose(serialize = false)
public TarType tarType;
}
public enum TarType {
@SerializedName("GZIP")
GZIP
}
}

View File

@@ -0,0 +1,87 @@
package com.java.utils;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* 微信支付 HTTP 客户端,封装了请求签名、发送、应答验签的完整流程。
* 依赖 WXPayUtility 提供的签名、验签、序列化等基础能力。
*/
public class WXPayClient {
private static final String HOST = "https://api.mch.weixin.qq.com";
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public WXPayClient(String mchid, String certificateSerialNo, String privateKeyFilePath,
String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
/**
* 发送 GET 请求,返回已验签的应答 Body
*/
public String sendGet(String uri) {
return sendRequest("GET", uri, null);
}
/**
* 发送 POST 请求,返回已验签的应答 Body
*/
public String sendPost(String uri, String reqBody) {
return sendRequest("POST", uri, reqBody);
}
/**
* 使用公钥加密敏感信息
*/
public String encrypt(String plainText) {
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
}
private String sendRequest(String method, String uri, String reqBody) {
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(
mchid, certificateSerialNo, privateKey, method, uri, reqBody));
if (reqBody != null) {
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(method, body);
} else {
reqBuilder.method(method, null);
}
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey,
httpResponse.headers(), respBody);
return respBody;
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
}

View File

@@ -0,0 +1,700 @@
package com.java.utils;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Map.Entry;
import okhttp3.Headers;
import okhttp3.Response;
import okio.BufferedSource;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.security.MessageDigest;
import java.io.InputStream;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
public class WXPayUtility {
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.addSerializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
return expose != null && !expose.serialize();
}
@Override
public boolean shouldSkipClass(Class<?> aClass) {
return false;
}
})
.addDeserializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
return expose != null && !expose.deserialize();
}
@Override
public boolean shouldSkipClass(Class<?> aClass) {
return false;
}
})
.create();
private static final char[] SYMBOLS =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
private static final SecureRandom random = new SecureRandom();
public static String toJson(Object object) {
return gson.toJson(object);
}
public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
return gson.fromJson(json, classOfT);
}
private static String readKeyStringFromPath(String keyPath) {
try {
return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static PrivateKey loadPrivateKeyFromString(String keyString) {
try {
keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
return KeyFactory.getInstance("RSA").generatePrivate(
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e);
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException(e);
}
}
public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
}
public static PublicKey loadPublicKeyFromString(String keyString) {
try {
keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
return KeyFactory.getInstance("RSA").generatePublic(
new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(e);
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException(e);
}
}
public static PublicKey loadPublicKeyFromPath(String keyPath) {
return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
}
public static String createNonce(int length) {
char[] buf = new char[length];
for (int i = 0; i < length; ++i) {
buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
}
return new String(buf);
}
public static String encrypt(PublicKey publicKey, String plaintext) {
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
try {
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new IllegalArgumentException("Plaintext is too long", e);
}
}
public static String rsaOaepDecrypt(PrivateKey privateKey, String ciphertext) {
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
try {
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("RSA decryption using an illegal privateKey", e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new IllegalArgumentException("Ciphertext decryption failed", e);
}
}
public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce,
byte[] ciphertext) {
final String transformation = "AES/GCM/NoPadding";
final String algorithm = "AES";
final int tagLengthBit = 128;
try {
Cipher cipher = Cipher.getInstance(transformation);
cipher.init(
Cipher.DECRYPT_MODE,
new SecretKeySpec(key, algorithm),
new GCMParameterSpec(tagLengthBit, nonce));
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
} catch (InvalidKeyException
| InvalidAlgorithmParameterException
| BadPaddingException
| IllegalBlockSizeException
| NoSuchAlgorithmException
| NoSuchPaddingException e) {
throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed",
transformation), e);
}
}
public static String sign(String message, String algorithm, PrivateKey privateKey) {
byte[] sign;
try {
Signature signature = Signature.getInstance(algorithm);
signature.initSign(privateKey);
signature.update(message.getBytes(StandardCharsets.UTF_8));
sign = signature.sign();
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
} catch (SignatureException e) {
throw new RuntimeException("An error occurred during the sign process.", e);
}
return Base64.getEncoder().encodeToString(sign);
}
public static boolean verify(String message, String signature, String algorithm,
PublicKey publicKey) {
try {
Signature sign = Signature.getInstance(algorithm);
sign.initVerify(publicKey);
sign.update(message.getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getDecoder().decode(signature));
} catch (SignatureException e) {
return false;
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("verify uses an illegal publickey.", e);
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
}
}
public static String buildAuthorization(String mchid, String certificateSerialNo,
PrivateKey privateKey,
String method, String uri, String body) {
String nonce = createNonce(32);
long timestamp = Instant.now().getEpochSecond();
String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
body == null ? "" : body);
String signature = sign(message, "SHA256withRSA", privateKey);
return String.format(
"WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
"timestamp=\"%d\",serial_no=\"%s\"",
mchid, nonce, signature, timestamp, certificateSerialNo);
}
private static String calculateHash(InputStream inputStream, String algorithm) {
try {
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
byte[] hashBytes = digest.digest();
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new UnsupportedOperationException(algorithm + " algorithm not available", e);
} catch (IOException e) {
throw new RuntimeException("Error reading from input stream", e);
}
}
public static String sha256(InputStream inputStream) {
return calculateHash(inputStream, "SHA-256");
}
public static String sha1(InputStream inputStream) {
return calculateHash(inputStream, "SHA-1");
}
public static String sm3(InputStream inputStream) {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
try {
SM3Digest digest = new SM3Digest();
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
byte[] hashBytes = new byte[digest.getDigestSize()];
digest.doFinal(hashBytes, 0);
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (IOException e) {
throw new RuntimeException("Error reading from input stream", e);
}
}
public static String urlEncode(String content) {
try {
return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static String urlEncode(Map<String, Object> params) {
if (params == null || params.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
for (Entry<String, Object> entry : params.entrySet()) {
if (entry.getValue() == null) {
continue;
}
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof List) {
List<?> list = (List<?>) entry.getValue();
for (Object temp : list) {
appendParam(result, key, temp);
}
} else {
appendParam(result, key, value);
}
}
return result.toString();
}
private static void appendParam(StringBuilder result, String key, Object value) {
if (result.length() > 0) {
result.append("&");
}
String valueString;
if (value instanceof String || value instanceof Number ||
value instanceof Boolean || value instanceof Enum) {
valueString = value.toString();
} else {
valueString = toJson(value);
}
result.append(key)
.append("=")
.append(urlEncode(valueString));
}
public static String extractBody(Response response) {
if (response.body() == null) {
return "";
}
try {
BufferedSource source = response.body().source();
return source.readUtf8();
} catch (IOException e) {
throw new RuntimeException(String.format("An error occurred during reading response body. " +
"Status: %d", response.code()), e);
}
}
public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
Headers headers,
String body) {
String timestamp = headers.get("Wechatpay-Timestamp");
String requestId = headers.get("Request-ID");
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
throw new IllegalArgumentException(
String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]",
timestamp, requestId));
}
} catch (DateTimeException | NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]",
timestamp, requestId));
}
String serialNumber = headers.get("Wechatpay-Serial");
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
throw new IllegalArgumentException(
String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " +
"%s", wechatpayPublicKeyId, serialNumber));
}
String signature = headers.get("Wechatpay-Signature");
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
body == null ? "" : body);
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
if (!success) {
throw new IllegalArgumentException(
String.format("Validate response failed,the WechatPay signature is incorrect.%n"
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
headers.get("Request-ID"), headers, body));
}
}
public static void validateNotification(String wechatpayPublicKeyId,
PublicKey wechatpayPublicKey, Headers headers,
String body) {
String timestamp = headers.get("Wechatpay-Timestamp");
try {
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
throw new IllegalArgumentException(
String.format("Validate notification failed, timestamp[%s] is expired", timestamp));
}
} catch (DateTimeException | NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Validate notification failed, timestamp[%s] is invalid", timestamp));
}
String serialNumber = headers.get("Wechatpay-Serial");
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
throw new IllegalArgumentException(
String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " +
"Remote: %s",
wechatpayPublicKeyId,
serialNumber));
}
String signature = headers.get("Wechatpay-Signature");
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
body == null ? "" : body);
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
if (!success) {
throw new IllegalArgumentException(
String.format("Validate notification failed, WechatPay signature is incorrect.\n"
+ "responseHeader[%s]\tresponseBody[%.1024s]",
headers, body));
}
}
public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId,
PublicKey wechatpayPublicKey, Headers headers,
String body) {
validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body);
Notification notification = gson.fromJson(body, Notification.class);
notification.decrypt(apiv3Key);
return notification;
}
public static class ApiException extends RuntimeException {
private static final long serialVersionUID = 2261086748874802175L;
private final int statusCode;
private final String body;
private final Headers headers;
private final String errorCode;
private final String errorMessage;
public ApiException(int statusCode, String body, Headers headers) {
super(String.format("微信支付API访问失败StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode,
body, headers));
this.statusCode = statusCode;
this.body = body;
this.headers = headers;
if (body != null && !body.isEmpty()) {
JsonElement code;
JsonElement message;
try {
JsonObject jsonObject = gson.fromJson(body, JsonObject.class);
code = jsonObject.get("code");
message = jsonObject.get("message");
} catch (JsonSyntaxException ignored) {
code = null;
message = null;
}
this.errorCode = code == null ? null : code.getAsString();
this.errorMessage = message == null ? null : message.getAsString();
} else {
this.errorCode = null;
this.errorMessage = null;
}
}
public int getStatusCode() {
return statusCode;
}
public String getBody() {
return body;
}
public Headers getHeaders() {
return headers;
}
public String getErrorCode() {
return errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
}
public static class Notification {
@SerializedName("id")
private String id;
@SerializedName("create_time")
private String createTime;
@SerializedName("event_type")
private String eventType;
@SerializedName("resource_type")
private String resourceType;
@SerializedName("summary")
private String summary;
@SerializedName("resource")
private Resource resource;
private String plaintext;
public String getId() {
return id;
}
public String getCreateTime() {
return createTime;
}
public String getEventType() {
return eventType;
}
public String getResourceType() {
return resourceType;
}
public String getSummary() {
return summary;
}
public Resource getResource() {
return resource;
}
public String getPlaintext() {
return plaintext;
}
private void validate() {
if (resource == null) {
throw new IllegalArgumentException("Missing required field `resource` in notification");
}
resource.validate();
}
private void decrypt(String apiv3Key) {
validate();
plaintext = aesAeadDecrypt(
apiv3Key.getBytes(StandardCharsets.UTF_8),
resource.associatedData.getBytes(StandardCharsets.UTF_8),
resource.nonce.getBytes(StandardCharsets.UTF_8),
Base64.getDecoder().decode(resource.ciphertext)
);
}
public static class Resource {
@SerializedName("algorithm")
private String algorithm;
@SerializedName("ciphertext")
private String ciphertext;
@SerializedName("associated_data")
private String associatedData;
@SerializedName("nonce")
private String nonce;
@SerializedName("original_type")
private String originalType;
public String getAlgorithm() {
return algorithm;
}
public String getCiphertext() {
return ciphertext;
}
public String getAssociatedData() {
return associatedData;
}
public String getNonce() {
return nonce;
}
public String getOriginalType() {
return originalType;
}
private void validate() {
if (algorithm == null || algorithm.isEmpty()) {
throw new IllegalArgumentException("Missing required field `algorithm` in Notification" +
".Resource");
}
if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) {
throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " +
"Notification.Resource", algorithm));
}
if (ciphertext == null || ciphertext.isEmpty()) {
throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" +
".Resource");
}
if (associatedData == null || associatedData.isEmpty()) {
throw new IllegalArgumentException("Missing required field `associatedData` in " +
"Notification.Resource");
}
if (nonce == null || nonce.isEmpty()) {
throw new IllegalArgumentException("Missing required field `nonce` in Notification" +
".Resource");
}
if (originalType == null || originalType.isEmpty()) {
throw new IllegalArgumentException("Missing required field `originalType` in " +
"Notification.Resource");
}
}
}
}
public static String getContentTypeByFileName(String fileName) {
if (fileName == null || fileName.isEmpty()) {
return "application/octet-stream";
}
String extension = "";
int lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
extension = fileName.substring(lastDotIndex + 1).toLowerCase();
}
Map<String, String> contentTypeMap = new HashMap<>();
contentTypeMap.put("png", "image/png");
contentTypeMap.put("jpg", "image/jpeg");
contentTypeMap.put("jpeg", "image/jpeg");
contentTypeMap.put("gif", "image/gif");
contentTypeMap.put("bmp", "image/bmp");
contentTypeMap.put("webp", "image/webp");
contentTypeMap.put("svg", "image/svg+xml");
contentTypeMap.put("ico", "image/x-icon");
contentTypeMap.put("pdf", "application/pdf");
contentTypeMap.put("doc", "application/msword");
contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
contentTypeMap.put("xls", "application/vnd.ms-excel");
contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
contentTypeMap.put("ppt", "application/vnd.ms-powerpoint");
contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
contentTypeMap.put("txt", "text/plain");
contentTypeMap.put("html", "text/html");
contentTypeMap.put("css", "text/css");
contentTypeMap.put("js", "application/javascript");
contentTypeMap.put("json", "application/json");
contentTypeMap.put("xml", "application/xml");
contentTypeMap.put("csv", "text/csv");
contentTypeMap.put("mp3", "audio/mpeg");
contentTypeMap.put("wav", "audio/wav");
contentTypeMap.put("mp4", "video/mp4");
contentTypeMap.put("avi", "video/x-msvideo");
contentTypeMap.put("mov", "video/quicktime");
contentTypeMap.put("zip", "application/zip");
contentTypeMap.put("rar", "application/x-rar-compressed");
contentTypeMap.put("7z", "application/x-7z-compressed");
return contentTypeMap.getOrDefault(extension, "application/octet-stream");
}
}

View File

@@ -0,0 +1,222 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 合单下单-APP
*/
public class UnionAppPrepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/combine-transactions/app";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
UnionAppPrepay client = new UnionAppPrepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
UnionAPIv3AppPrepayRequest request = new UnionAPIv3AppPrepayRequest();
request.combineAppid = "wxd678efh567hg6787";
request.combineOutTradeNo = "20150806125345";
request.combineMchid = "1900000109";
request.sceneInfo = new UnionSceneInfo();
request.sceneInfo.deviceId = "POS1:1";
request.sceneInfo.payerClientIp = "14.17.22.32";
request.subOrders = new ArrayList<>();
{
UnionSubOrder subOrdersItem0 = new UnionSubOrder();
subOrdersItem0.mchid = "1230000109";
subOrdersItem0.outTradeNo = "20150806125346";
subOrdersItem0.amount = new UnionAmountInfo();
subOrdersItem0.amount.totalAmount = 10L;
subOrdersItem0.amount.currency = "CNY";
subOrdersItem0.attach = "深圳分店";
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
subOrdersItem0.detail = "买单费用";
subOrdersItem0.goodsTag = "WXG";
subOrdersItem0.settleInfo = new UnionSettleInfo();
subOrdersItem0.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem0);
UnionSubOrder subOrdersItem1 = new UnionSubOrder();
subOrdersItem1.mchid = "1230000119";
subOrdersItem1.outTradeNo = "20150806125347";
subOrdersItem1.amount = new UnionAmountInfo();
subOrdersItem1.amount.totalAmount = 10L;
subOrdersItem1.amount.currency = "CNY";
subOrdersItem1.attach = "广州分店";
subOrdersItem1.description = "腾讯充值中心-微信充值";
subOrdersItem1.detail = "买单费用";
subOrdersItem1.goodsTag = "WXG";
subOrdersItem1.settleInfo = new UnionSettleInfo();
subOrdersItem1.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem1);
};
request.combinePayerInfo = new UnionAppPayerInfo();
request.combinePayerInfo.openid = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o";
request.timeExpire = "2000-01-01T00:00:00+08:00";
request.notifyUrl = "https://yourapp.com/notify";
try {
UnionAPIv3AppPrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public UnionAPIv3AppPrepayResponse run(UnionAPIv3AppPrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, UnionAPIv3AppPrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public UnionAppPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class UnionAPIv3AppPrepayRequest {
@SerializedName("combine_appid")
public String combineAppid;
@SerializedName("combine_out_trade_no")
public String combineOutTradeNo;
@SerializedName("combine_mchid")
public String combineMchid;
@SerializedName("scene_info")
public UnionSceneInfo sceneInfo;
@SerializedName("sub_orders")
public List<UnionSubOrder> subOrders = new ArrayList<UnionSubOrder>();
@SerializedName("combine_payer_info")
public UnionAppPayerInfo combinePayerInfo;
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("notify_url")
public String notifyUrl;
@SerializedName("trade_scenario")
public String tradeScenario;
}
public static class UnionAPIv3AppPrepayResponse {
@SerializedName("prepay_id")
public String prepayId;
}
public static class UnionSceneInfo {
@SerializedName("device_id")
public String deviceId;
@SerializedName("payer_client_ip")
public String payerClientIp;
}
public static class UnionSubOrder {
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("amount")
public UnionAmountInfo amount;
@SerializedName("attach")
public String attach;
@SerializedName("description")
public String description;
@SerializedName("detail")
public String detail;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("settle_info")
public UnionSettleInfo settleInfo;
}
public static class UnionAppPayerInfo {
@SerializedName("openid")
public String openid;
}
public static class UnionAmountInfo {
@SerializedName("total_amount")
public Long totalAmount;
@SerializedName("currency")
public String currency;
}
public static class UnionSettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
}

View File

@@ -0,0 +1,124 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 合单关单
*/
public class UnionClose {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
UnionClose client = new UnionClose(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
UnionCloseRequest request = new UnionCloseRequest();
request.combineOutTradeNo = "1217752501201407033233368018";
request.combineAppid = "wxd678efh567hg6787";
request.subOrders = new ArrayList<>();
{
UnionCloseSubOrder subOrdersItem = new UnionCloseSubOrder();
subOrdersItem.mchid = "1900000109";
subOrdersItem.outTradeNo = "20150806125346";
request.subOrders.add(subOrdersItem);
};
try {
client.run(request);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public void run(UnionCloseRequest request) {
String uri = PATH;
uri = uri.replace("{combine_out_trade_no}", WXPayUtility.urlEncode(request.combineOutTradeNo));
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
return;
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public UnionClose(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class UnionCloseRequest {
@SerializedName("combine_appid")
public String combineAppid;
@SerializedName("combine_out_trade_no")
@Expose(serialize = false)
public String combineOutTradeNo;
@SerializedName("sub_orders")
public List<UnionCloseSubOrder> subOrders = new ArrayList<UnionCloseSubOrder>();
}
public static class UnionCloseSubOrder {
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
public String outTradeNo;
}
}

View File

@@ -0,0 +1,230 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 合单下单-H5
*/
public class UnionH5Prepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/combine-transactions/h5";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
UnionH5Prepay client = new UnionH5Prepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
UnionAPIv3H5PrepayRequest request = new UnionAPIv3H5PrepayRequest();
request.combineAppid = "wxd678efh567hg6787";
request.combineOutTradeNo = "1217752501201407033233368018";
request.combineMchid = "1230000109";
request.sceneInfo = new UnionH5SceneInfo();
request.sceneInfo.payerClientIp = "14.23.150.211";
request.sceneInfo.deviceId = "013467007045764";
request.sceneInfo.h5Info = new UnionH5Info();
request.sceneInfo.h5Info.type = "iOS";
request.sceneInfo.h5Info.appName = "王者荣耀";
request.sceneInfo.h5Info.appUrl = "https://pay.qq.com";
request.sceneInfo.h5Info.bundleId = "com.tencent.wzryiOS";
request.sceneInfo.h5Info.packageName = "com.tencent.tmgp.sgame";
request.subOrders = new ArrayList<>();
{
UnionCommonSubOrder subOrdersItem0 = new UnionCommonSubOrder();
subOrdersItem0.mchid = "1230000109";
subOrdersItem0.outTradeNo = "20150806125346";
subOrdersItem0.amount = new UnionAmountInfo();
subOrdersItem0.amount.totalAmount = 10L;
subOrdersItem0.amount.currency = "CNY";
subOrdersItem0.attach = "深圳分店";
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
subOrdersItem0.goodsTag = "WXG";
subOrdersItem0.settleInfo = new UnionSettleInfo();
subOrdersItem0.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem0);
UnionCommonSubOrder subOrdersItem1 = new UnionCommonSubOrder();
subOrdersItem1.mchid = "1230000119";
subOrdersItem1.outTradeNo = "20150806125347";
subOrdersItem1.amount = new UnionAmountInfo();
subOrdersItem1.amount.totalAmount = 10L;
subOrdersItem1.amount.currency = "CNY";
subOrdersItem1.attach = "广州分店";
subOrdersItem1.description = "腾讯充值中心-微信充值";
subOrdersItem1.goodsTag = "WXG";
subOrdersItem1.settleInfo = new UnionSettleInfo();
subOrdersItem1.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem1);
};
request.timeExpire = "2018-06-08T10:34:56+08:00";
request.notifyUrl = "https://yourapp.com/notify";
try {
UnionAPIv3H5PrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public UnionAPIv3H5PrepayResponse run(UnionAPIv3H5PrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, UnionAPIv3H5PrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public UnionH5Prepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class UnionAPIv3H5PrepayRequest {
@SerializedName("combine_appid")
public String combineAppid;
@SerializedName("combine_out_trade_no")
public String combineOutTradeNo;
@SerializedName("combine_mchid")
public String combineMchid;
@SerializedName("scene_info")
public UnionH5SceneInfo sceneInfo;
@SerializedName("sub_orders")
public List<UnionCommonSubOrder> subOrders = new ArrayList<UnionCommonSubOrder>();
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("notify_url")
public String notifyUrl;
}
public static class UnionAPIv3H5PrepayResponse {
@SerializedName("h5_url")
public String h5Url;
}
public static class UnionH5SceneInfo {
@SerializedName("payer_client_ip")
public String payerClientIp;
@SerializedName("device_id")
public String deviceId;
@SerializedName("h5_info")
public UnionH5Info h5Info;
}
public static class UnionCommonSubOrder {
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("amount")
public UnionAmountInfo amount;
@SerializedName("attach")
public String attach;
@SerializedName("description")
public String description;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("settle_info")
public UnionSettleInfo settleInfo;
}
public static class UnionH5Info {
@SerializedName("type")
public String type;
@SerializedName("app_name")
public String appName;
@SerializedName("app_url")
public String appUrl;
@SerializedName("bundle_id")
public String bundleId;
@SerializedName("package_name")
public String packageName;
}
public static class UnionAmountInfo {
@SerializedName("total_amount")
public Long totalAmount;
@SerializedName("currency")
public String currency;
}
public static class UnionSettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
}

View File

@@ -0,0 +1,222 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 合单下单-JSAPI
*/
public class UnionJsapiPrepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/combine-transactions/jsapi";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
UnionJsapiPrepay client = new UnionJsapiPrepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
UnionAPIv3JsapiPrepayRequest request = new UnionAPIv3JsapiPrepayRequest();
request.combineAppid = "wxd678efh567hg6787";
request.combineMchid = "1230000109";
request.combineOutTradeNo = "1217752501201407033233368018";
request.combinePayerInfo = new UnionPayerInfo();
request.combinePayerInfo.openid = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o";
request.sceneInfo = new UnionSceneInfo();
request.sceneInfo.deviceId = "POS1:1";
request.sceneInfo.payerClientIp = "14.17.22.32";
request.subOrders = new ArrayList<>();
{
UnionSubOrder subOrdersItem0 = new UnionSubOrder();
subOrdersItem0.mchid = "1230000109";
subOrdersItem0.outTradeNo = "20150806125346";
subOrdersItem0.amount = new UnionAmountInfo();
subOrdersItem0.amount.totalAmount = 10L;
subOrdersItem0.amount.currency = "CNY";
subOrdersItem0.attach = "深圳分店";
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
subOrdersItem0.detail = "买单费用";
subOrdersItem0.goodsTag = "WXG";
subOrdersItem0.settleInfo = new UnionSettleInfo();
subOrdersItem0.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem0);
UnionSubOrder subOrdersItem1 = new UnionSubOrder();
subOrdersItem1.mchid = "1230000119";
subOrdersItem1.outTradeNo = "20150806125347";
subOrdersItem1.amount = new UnionAmountInfo();
subOrdersItem1.amount.totalAmount = 10L;
subOrdersItem1.amount.currency = "CNY";
subOrdersItem1.attach = "广州分店";
subOrdersItem1.description = "腾讯充值中心-微信充值";
subOrdersItem1.detail = "买单费用";
subOrdersItem1.goodsTag = "WXG";
subOrdersItem1.settleInfo = new UnionSettleInfo();
subOrdersItem1.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem1);
};
request.timeExpire = "2018-06-08T10:34:56+08:00";
request.notifyUrl = "https://yourapp.com/notify";
try {
UnionAPIv3JsapiPrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public UnionAPIv3JsapiPrepayResponse run(UnionAPIv3JsapiPrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, UnionAPIv3JsapiPrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public UnionJsapiPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class UnionAPIv3JsapiPrepayRequest {
@SerializedName("combine_appid")
public String combineAppid;
@SerializedName("combine_mchid")
public String combineMchid;
@SerializedName("combine_out_trade_no")
public String combineOutTradeNo;
@SerializedName("combine_payer_info")
public UnionPayerInfo combinePayerInfo;
@SerializedName("scene_info")
public UnionSceneInfo sceneInfo;
@SerializedName("sub_orders")
public List<UnionSubOrder> subOrders = new ArrayList<UnionSubOrder>();
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("notify_url")
public String notifyUrl;
}
public static class UnionAPIv3JsapiPrepayResponse {
@SerializedName("prepay_id")
public String prepayId;
}
public static class UnionPayerInfo {
@SerializedName("openid")
public String openid;
@SerializedName("sub_openid")
public String subOpenid;
}
public static class UnionSceneInfo {
@SerializedName("device_id")
public String deviceId;
@SerializedName("payer_client_ip")
public String payerClientIp;
}
public static class UnionSubOrder {
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("amount")
public UnionAmountInfo amount;
@SerializedName("attach")
public String attach;
@SerializedName("description")
public String description;
@SerializedName("detail")
public String detail;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("settle_info")
public UnionSettleInfo settleInfo;
}
public static class UnionAmountInfo {
@SerializedName("total_amount")
public Long totalAmount;
@SerializedName("currency")
public String currency;
}
public static class UnionSettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
}

View File

@@ -0,0 +1,204 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 合单下单-NATIVE
*/
public class UnionNativePrepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/combine-transactions/native";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
UnionNativePrepay client = new UnionNativePrepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
UnionAPIv3NativePrepayRequest request = new UnionAPIv3NativePrepayRequest();
request.combineAppid = "wxd678efh567hg6787";
request.combineOutTradeNo = "20150806125346";
request.combineMchid = "1900000109";
request.sceneInfo = new UnionSceneInfo();
request.sceneInfo.deviceId = "POS1:1";
request.sceneInfo.payerClientIp = "14.17.22.32";
request.subOrders = new ArrayList<>();
{
UnionCommonSubOrder subOrdersItem0 = new UnionCommonSubOrder();
subOrdersItem0.mchid = "1230000109";
subOrdersItem0.outTradeNo = "20150806125346";
subOrdersItem0.amount = new UnionAmountInfo();
subOrdersItem0.amount.totalAmount = 10L;
subOrdersItem0.amount.currency = "CNY";
subOrdersItem0.attach = "深圳分店";
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
subOrdersItem0.goodsTag = "WXG";
subOrdersItem0.settleInfo = new UnionSettleInfo();
subOrdersItem0.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem0);
UnionCommonSubOrder subOrdersItem1 = new UnionCommonSubOrder();
subOrdersItem1.mchid = "1230000119";
subOrdersItem1.outTradeNo = "20150806125347";
subOrdersItem1.amount = new UnionAmountInfo();
subOrdersItem1.amount.totalAmount = 10L;
subOrdersItem1.amount.currency = "CNY";
subOrdersItem1.attach = "广州分店";
subOrdersItem1.description = "腾讯充值中心-微信充值";
subOrdersItem1.goodsTag = "WXG";
subOrdersItem1.settleInfo = new UnionSettleInfo();
subOrdersItem1.settleInfo.profitSharing = false;
request.subOrders.add(subOrdersItem1);
};
request.timeExpire = "2000-01-01T00:00:00+08:00";
request.notifyUrl = "https://yourapp.com/notify";
try {
UnionAPIv3NativePrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public UnionAPIv3NativePrepayResponse run(UnionAPIv3NativePrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, UnionAPIv3NativePrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public UnionNativePrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class UnionAPIv3NativePrepayRequest {
@SerializedName("combine_appid")
public String combineAppid;
@SerializedName("combine_out_trade_no")
public String combineOutTradeNo;
@SerializedName("combine_mchid")
public String combineMchid;
@SerializedName("scene_info")
public UnionSceneInfo sceneInfo;
@SerializedName("sub_orders")
public List<UnionCommonSubOrder> subOrders = new ArrayList<UnionCommonSubOrder>();
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("notify_url")
public String notifyUrl;
}
public static class UnionAPIv3NativePrepayResponse {
@SerializedName("code_url")
public String codeUrl;
}
public static class UnionSceneInfo {
@SerializedName("device_id")
public String deviceId;
@SerializedName("payer_client_ip")
public String payerClientIp;
}
public static class UnionCommonSubOrder {
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("amount")
public UnionAmountInfo amount;
@SerializedName("attach")
public String attach;
@SerializedName("description")
public String description;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("settle_info")
public UnionSettleInfo settleInfo;
}
public static class UnionAmountInfo {
@SerializedName("total_amount")
public Long totalAmount;
@SerializedName("currency")
public String currency;
}
public static class UnionSettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
}

View File

@@ -0,0 +1,242 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 商户订单号查询订单
*/
public class UnionQueryByOutTradeNo {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
UnionQueryByOutTradeNo client = new UnionQueryByOutTradeNo(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
UnionQueryByOutTradeNoRequest request = new UnionQueryByOutTradeNoRequest();
request.combineOutTradeNo = "P20150806125346";
try {
UnionAPIv3UnionQueryResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public UnionAPIv3UnionQueryResponse run(UnionQueryByOutTradeNoRequest request) {
String uri = PATH;
uri = uri.replace("{combine_out_trade_no}", WXPayUtility.urlEncode(request.combineOutTradeNo));
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, UnionAPIv3UnionQueryResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public UnionQueryByOutTradeNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class UnionQueryByOutTradeNoRequest {
@SerializedName("combine_out_trade_no")
@Expose(serialize = false)
public String combineOutTradeNo;
}
public static class UnionAPIv3UnionQueryResponse {
@SerializedName("combine_appid")
public String combineAppid;
@SerializedName("combine_mchid")
public String combineMchid;
@SerializedName("combine_out_trade_no")
public String combineOutTradeNo;
@SerializedName("combine_payer_info")
public UnionCommRespPayerInfo combinePayerInfo;
@SerializedName("scene_info")
public UnionCommRespSceneInfo sceneInfo;
@SerializedName("sub_orders")
public List<UnionSubOrder> subOrders;
}
public static class UnionCommRespPayerInfo {
@SerializedName("openid")
public String openid;
}
public static class UnionCommRespSceneInfo {
@SerializedName("device_id")
public String deviceId;
}
public static class UnionSubOrder {
@SerializedName("mchid")
public String mchid;
@SerializedName("sub_mchid")
public String subMchid;
@SerializedName("sub_appid")
public String subAppid;
@SerializedName("sub_openid")
public String subOpenid;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("trade_type")
public String tradeType;
@SerializedName("trade_state")
public String tradeState;
@SerializedName("bank_type")
public String bankType;
@SerializedName("attach")
public String attach;
@SerializedName("success_time")
public String successTime;
@SerializedName("amount")
public UnionCommRespAmountInfo amount;
@SerializedName("promotion_detail")
public List<UnionPromotionDetail> promotionDetail;
}
public static class UnionCommRespAmountInfo {
@SerializedName("total_amount")
public Long totalAmount;
@SerializedName("payer_amount")
public Long payerAmount;
@SerializedName("currency")
public String currency;
@SerializedName("payer_currency")
public String payerCurrency;
@SerializedName("settlement_rate")
public Long settlementRate;
}
public static class UnionPromotionDetail {
@SerializedName("coupon_id")
public String couponId;
@SerializedName("name")
public String name;
@SerializedName("scope")
public String scope;
@SerializedName("type")
public String type;
@SerializedName("amount")
public Long amount;
@SerializedName("stock_id")
public String stockId;
@SerializedName("wechatpay_contribute")
public Long wechatpayContribute;
@SerializedName("merchant_contribute")
public Long merchantContribute;
@SerializedName("other_contribute")
public Long otherContribute;
@SerializedName("currency")
public String currency;
@SerializedName("goods_detail")
public List<GoodsDetailInPromotion> goodsDetail;
}
public static class GoodsDetailInPromotion {
@SerializedName("goods_id")
public String goodsId;
@SerializedName("quantity")
public Long quantity;
@SerializedName("unit_price")
public Long unitPrice;
@SerializedName("discount_amount")
public Long discountAmount;
@SerializedName("goods_remark")
public String goodsRemark;
}
}

View File

@@ -0,0 +1,118 @@
# 合单订单支付成功回调通知
## 回调描述
用户使用合单支付APP合单支付/H5合单支付/JSAPI合单支付/Native合单支付/小程序合单支付功能当用户成功支付订单后微信支付会通过POST的请求方式向商户预先设置的回调地址APP合单支付/H5合单支付/JSAPI合单支付/Native合单支付/小程序合单支付下单接口传入的notify_url发送回调通知让商户知晓用户已完成支付。
> **注意**商户侧对微信支付回调IP有防火墙策略限制的需要对微信回调IP段开通白名单否则会导致收不到回调微信支付回调被商户防火墙拦截详情参考回调处理逻辑注意事项。
## 回调报文格式
微信支付会通过POST的方式向回调地址发送回调报文回调通知的请求主体中会包含JSON格式的通知参数
```json
{
"id": "EV-2018022511223320873",
"create_time": "2015-05-20T13:29:35+08:00",
"resource_type": "encrypt-resource",
"event_type": "TRANSACTION.SUCCESS",
"summary": "支付成功",
"resource": {
"original_type": "transaction",
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "",
"associated_data": "",
"nonce": ""
}
}
```
## 关键字段说明
| 字段 | 说明 |
|------|------|
| `event_type` | 事件类型,支付成功为 `TRANSACTION.SUCCESS` |
| `resource.algorithm` | 加密算法,固定为 `AEAD_AES_256_GCM` |
| `resource.ciphertext` | 密文需使用商户APIv3密钥解密后得到合单订单详情 |
| `resource.associated_data` | 附加数据,解密时作为 AAD 参数 |
| `resource.nonce` | 随机串,解密时作为 Nonce 参数 |
## 解密后的合单订单明文结构
> 与基础支付回调的关键区别:解密后返回的是 `sub_orders[]` 数组(含各子单状态),而非单个订单。
```json
{
"combine_appid": "wxd678efh567hg6787",
"combine_out_trade_no": "20150806125346",
"combine_mchid": "1900000109",
"scene_info": {
"device_id": "POS1:1"
},
"sub_orders": [
{
"mchid": "1900000109",
"trade_type": "JSAPI",
"trade_state": "SUCCESS",
"bank_type": "CMC",
"attach": "深圳分店",
"amount": {
"total_amount": 10,
"currency": "CNY",
"payer_amount": 10,
"payer_currency": "CNY"
},
"success_time": "2015-05-20T13:29:35.120+08:00",
"transaction_id": "1009660380201506130728806387",
"out_trade_no": "20150806125346"
},
{
"mchid": "1900000179",
"trade_type": "JSAPI",
"trade_state": "SUCCESS",
"bank_type": "CMC",
"attach": "广州分店",
"amount": {
"total_amount": 10,
"currency": "CNY",
"payer_amount": 10,
"payer_currency": "CNY"
},
"success_time": "2015-05-20T13:29:35.120+08:00",
"transaction_id": "1009660380201506130728452147",
"out_trade_no": "20150806124855"
}
],
"combine_payer_info": {
"openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
}
}
```
## 解密后关键字段说明
| 字段 | 说明 |
|------|------|
| `combine_appid` | 合单发起方的 APPID |
| `combine_mchid` | 合单发起方的商户号 |
| `combine_out_trade_no` | 合单商户订单号 |
| `sub_orders[].mchid` | 子单商户号 |
| `sub_orders[].trade_type` | 交易类型:`JSAPI``NATIVE``APP``MWEB` |
| `sub_orders[].trade_state` | 交易状态:`SUCCESS`(支付成功)、`NOTPAY`(未支付)、`CLOSED`(已关闭) |
| `sub_orders[].transaction_id` | 子单微信支付订单号(退款时需用此字段) |
| `sub_orders[].out_trade_no` | 子单商户订单号 |
| `sub_orders[].amount.total_amount` | 子单金额,单位:分 |
| `sub_orders[].amount.payer_amount` | 用户实付金额(= total_amount - 代金券优惠) |
| `sub_orders[].success_time` | 支付完成时间rfc3339 格式 |
| `sub_orders[].bank_type` | 银行类型,非银行卡支付统一返回 `OTHERS` |
| `combine_payer_info.openid` | 实际支付用户在 `combine_appid` 下的 openid |
## 处理要求
1. 接收到回调后,先验证请求签名(使用微信支付公钥验签),验签机制与基础支付相同
2. 使用商户APIv3密钥 + AEAD_AES_256_GCM 解密 `resource.ciphertext` 得到合单订单明文
3. 验签通过后返回 HTTP 200 或 204无应答报文表示确认收到
4. 验签不通过返回 HTTP 5XX 或 4XX + `{"code": "FAIL", "message": "失败原因"}`
5. 若返回非200或超时5秒微信支付按策略重试15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h最多15次
6. 不可仅依赖回调获取支付结果,需结合**查询合单订单接口**使用
7. 收到重复回调通知时需做好幂等处理,持续应答 200

View File

@@ -0,0 +1,174 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 添加分账接收方
*/
public class AddReceiver {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/profitsharing/receivers/add";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
AddReceiver client = new AddReceiver(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
AddReceiverRequest request = new AddReceiverRequest();
request.appid = "wx8888888888888888";
request.type = ReceiverType.MERCHANT_ID;
request.account = "86693852";
request.name = client.encrypt("name");
request.relationType = ReceiverRelationType.STORE;
request.customRelation = "代理商";
try {
AddReceiverResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public AddReceiverResponse run(AddReceiverRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, AddReceiverResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public AddReceiver(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public String encrypt(String plainText) {
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
}
public static class AddReceiverRequest {
@SerializedName("appid")
public String appid;
@SerializedName("type")
public ReceiverType type;
@SerializedName("account")
public String account;
@SerializedName("name")
public String name;
@SerializedName("relation_type")
public ReceiverRelationType relationType;
@SerializedName("custom_relation")
public String customRelation;
}
public static class AddReceiverResponse {
@SerializedName("type")
public ReceiverType type;
@SerializedName("account")
public String account;
@SerializedName("name")
public String name;
@SerializedName("relation_type")
public ReceiverRelationType relationType;
@SerializedName("custom_relation")
public String customRelation;
}
public enum ReceiverType {
@SerializedName("MERCHANT_ID")
MERCHANT_ID,
@SerializedName("PERSONAL_OPENID")
PERSONAL_OPENID
}
public enum ReceiverRelationType {
@SerializedName("STORE")
STORE,
@SerializedName("STAFF")
STAFF,
@SerializedName("STORE_OWNER")
STORE_OWNER,
@SerializedName("PARTNER")
PARTNER,
@SerializedName("HEADQUARTER")
HEADQUARTER,
@SerializedName("BRAND")
BRAND,
@SerializedName("DISTRIBUTOR")
DISTRIBUTOR,
@SerializedName("USER")
USER,
@SerializedName("SUPPLIER")
SUPPLIER,
@SerializedName("CUSTOM")
CUSTOM
}
}

View File

@@ -0,0 +1,198 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 请求分账
*/
public class CreateOrder {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/profitsharing/orders";
public static void main(String[] args) {
CreateOrder client = new CreateOrder(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem"
);
CreateOrderRequest request = new CreateOrderRequest();
request.appid = "wx8888888888888888";
request.transactionId = "4208450740201411110007820472";
request.outOrderNo = "P20150806125346";
request.receivers = new ArrayList<>();
{
CreateOrderReceiver receiversItem = new CreateOrderReceiver();
receiversItem.type = "MERCHANT_ID";
receiversItem.account = "86693852";
receiversItem.name = client.encrypt("name");
receiversItem.amount = 888L;
receiversItem.description = "分给商户A";
request.receivers.add(receiversItem);
};
request.unfreezeUnsplit = true;
try {
OrdersEntity response = client.run(request);
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
e.printStackTrace();
}
}
public OrdersEntity run(CreateOrderRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
return WXPayUtility.fromJson(respBody, OrdersEntity.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public CreateOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public String encrypt(String plainText) {
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
}
public static class CreateOrderRequest {
@SerializedName("appid")
public String appid;
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("receivers")
public List<CreateOrderReceiver> receivers = new ArrayList<CreateOrderReceiver>();
@SerializedName("unfreeze_unsplit")
public Boolean unfreezeUnsplit;
}
public static class OrdersEntity {
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("order_id")
public String orderId;
@SerializedName("state")
public OrderStatus state;
@SerializedName("receivers")
public List<OrderReceiverDetail> receivers = new ArrayList<OrderReceiverDetail>();
}
public static class CreateOrderReceiver {
@SerializedName("type")
public String type;
@SerializedName("account")
public String account;
@SerializedName("name")
public String name;
@SerializedName("amount")
public Long amount;
@SerializedName("description")
public String description;
}
public enum OrderStatus {
@SerializedName("PROCESSING") PROCESSING,
@SerializedName("FINISHED") FINISHED
}
public static class OrderReceiverDetail {
@SerializedName("amount") public Long amount;
@SerializedName("description") public String description;
@SerializedName("type") public ReceiverType type;
@SerializedName("account") public String account;
@SerializedName("result") public DetailStatus result;
@SerializedName("fail_reason") public DetailFailReason failReason;
@SerializedName("create_time") public String createTime;
@SerializedName("finish_time") public String finishTime;
@SerializedName("detail_id") public String detailId;
}
public enum ReceiverType {
@SerializedName("MERCHANT_ID") MERCHANT_ID,
@SerializedName("PERSONAL_OPENID") PERSONAL_OPENID
}
public enum DetailStatus {
@SerializedName("PENDING") PENDING,
@SerializedName("SUCCESS") SUCCESS,
@SerializedName("CLOSED") CLOSED
}
public enum DetailFailReason {
@SerializedName("ACCOUNT_ABNORMAL") ACCOUNT_ABNORMAL,
@SerializedName("NO_RELATION") NO_RELATION,
@SerializedName("RECEIVER_HIGH_RISK") RECEIVER_HIGH_RISK,
@SerializedName("RECEIVER_REAL_NAME_NOT_VERIFIED") RECEIVER_REAL_NAME_NOT_VERIFIED,
@SerializedName("NO_AUTH") NO_AUTH,
@SerializedName("RECEIVER_RECEIPT_LIMIT") RECEIVER_RECEIPT_LIMIT,
@SerializedName("PAYER_ACCOUNT_ABNORMAL") PAYER_ACCOUNT_ABNORMAL,
@SerializedName("INVALID_REQUEST") INVALID_REQUEST
}
}

View File

@@ -0,0 +1,180 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 请求分账回退
*/
public class CreateReturnOrder {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/profitsharing/return-orders";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
CreateReturnOrder client = new CreateReturnOrder(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
CreateReturnOrderRequest request = new CreateReturnOrderRequest();
request.orderId = "3008450740201411110007820472";
request.outOrderNo = "P20150806125346";
request.outReturnNo = "R20190516001";
request.returnMchid = "86693852";
request.amount = 10L;
request.description = "用户退款";
try {
ReturnOrdersEntity response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public ReturnOrdersEntity run(CreateReturnOrderRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, ReturnOrdersEntity.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public CreateReturnOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class CreateReturnOrderRequest {
@SerializedName("order_id")
public String orderId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("out_return_no")
public String outReturnNo;
@SerializedName("return_mchid")
public String returnMchid;
@SerializedName("amount")
public Long amount;
@SerializedName("description")
public String description;
}
public static class ReturnOrdersEntity {
@SerializedName("order_id")
public String orderId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("out_return_no")
public String outReturnNo;
@SerializedName("return_id")
public String returnId;
@SerializedName("return_mchid")
public String returnMchid;
@SerializedName("amount")
public Long amount;
@SerializedName("description")
public String description;
@SerializedName("result")
public ReturnOrderStatus result;
@SerializedName("fail_reason")
public ReturnOrderFailReason failReason;
@SerializedName("create_time")
public String createTime;
@SerializedName("finish_time")
public String finishTime;
}
public enum ReturnOrderStatus {
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("FAILED")
FAILED
}
public enum ReturnOrderFailReason {
@SerializedName("ACCOUNT_ABNORMAL")
ACCOUNT_ABNORMAL,
@SerializedName("BALANCE_NOT_ENOUGH")
BALANCE_NOT_ENOUGH,
@SerializedName("TIME_OUT_CLOSED")
TIME_OUT_CLOSED,
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
PAYER_ACCOUNT_ABNORMAL,
@SerializedName("INVALID_REQUEST")
INVALID_REQUEST
}
}

View File

@@ -0,0 +1,126 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 删除分账接收方
*/
public class DeleteReceiver {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/profitsharing/receivers/delete";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
DeleteReceiver client = new DeleteReceiver(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
DeleteReceiverRequest request = new DeleteReceiverRequest();
request.appid = "wx8888888888888888";
request.type = ReceiverType.MERCHANT_ID;
request.account = "1900000109";
try {
DeleteReceiverResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public DeleteReceiverResponse run(DeleteReceiverRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, DeleteReceiverResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public DeleteReceiver(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class DeleteReceiverRequest {
@SerializedName("appid")
public String appid;
@SerializedName("type")
public ReceiverType type;
@SerializedName("account")
public String account;
}
public static class DeleteReceiverResponse {
@SerializedName("type")
public ReceiverType type;
@SerializedName("account")
public String account;
}
public enum ReceiverType {
@SerializedName("MERCHANT_ID")
MERCHANT_ID,
@SerializedName("PERSONAL_OPENID")
PERSONAL_OPENID
}
}

View File

@@ -0,0 +1,201 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 查询分账结果
*/
public class QueryOrder {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/profitsharing/orders/{out_order_no}";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
QueryOrder client = new QueryOrder(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
QueryOrderRequest request = new QueryOrderRequest();
request.outOrderNo = "P20150806125346";
request.transactionId = "4208450740201411110007820472";
try {
OrdersEntity response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public OrdersEntity run(QueryOrderRequest request) {
String uri = PATH;
uri = uri.replace("{out_order_no}", WXPayUtility.urlEncode(request.outOrderNo));
Map<String, Object> args = new HashMap<>();
args.put("transaction_id", request.transactionId);
String queryString = WXPayUtility.urlEncode(args);
if (!queryString.isEmpty()) {
uri = uri + "?" + queryString;
}
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, OrdersEntity.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public QueryOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class QueryOrderRequest {
@SerializedName("transaction_id")
@Expose(serialize = false)
public String transactionId;
@SerializedName("out_order_no")
@Expose(serialize = false)
public String outOrderNo;
}
public static class OrdersEntity {
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("order_id")
public String orderId;
@SerializedName("state")
public OrderStatus state;
@SerializedName("receivers")
public List<OrderReceiverDetail> receivers = new ArrayList<OrderReceiverDetail>();
}
public enum OrderStatus {
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("FINISHED")
FINISHED
}
public static class OrderReceiverDetail {
@SerializedName("amount")
public Long amount;
@SerializedName("description")
public String description;
@SerializedName("type")
public ReceiverType type;
@SerializedName("account")
public String account;
@SerializedName("result")
public DetailStatus result;
@SerializedName("fail_reason")
public DetailFailReason failReason;
@SerializedName("create_time")
public String createTime;
@SerializedName("finish_time")
public String finishTime;
@SerializedName("detail_id")
public String detailId;
}
public enum ReceiverType {
@SerializedName("MERCHANT_ID")
MERCHANT_ID,
@SerializedName("PERSONAL_OPENID")
PERSONAL_OPENID
}
public enum DetailStatus {
@SerializedName("PENDING")
PENDING,
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("CLOSED")
CLOSED
}
public enum DetailFailReason {
@SerializedName("ACCOUNT_ABNORMAL")
ACCOUNT_ABNORMAL,
@SerializedName("NO_RELATION")
NO_RELATION,
@SerializedName("RECEIVER_HIGH_RISK")
RECEIVER_HIGH_RISK,
@SerializedName("RECEIVER_REAL_NAME_NOT_VERIFIED")
RECEIVER_REAL_NAME_NOT_VERIFIED,
@SerializedName("NO_AUTH")
NO_AUTH,
@SerializedName("RECEIVER_RECEIPT_LIMIT")
RECEIVER_RECEIPT_LIMIT,
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
PAYER_ACCOUNT_ABNORMAL,
@SerializedName("INVALID_REQUEST")
INVALID_REQUEST
}
}

View File

@@ -0,0 +1,110 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 查询剩余待分金额
*/
public class QueryOrderAmount {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/profitsharing/transactions/{transaction_id}/amounts";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
QueryOrderAmount client = new QueryOrderAmount(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
QueryOrderAmountRequest request = new QueryOrderAmountRequest();
request.transactionId = "4208450740201411110007820472";
try {
QueryOrderAmountResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public QueryOrderAmountResponse run(QueryOrderAmountRequest request) {
String uri = PATH;
uri = uri.replace("{transaction_id}", WXPayUtility.urlEncode(request.transactionId));
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, QueryOrderAmountResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public QueryOrderAmount(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class QueryOrderAmountRequest {
@SerializedName("transaction_id")
@Expose(serialize = false)
public String transactionId;
}
public static class QueryOrderAmountResponse {
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("unsplit_amount")
public Long unsplitAmount;
}
}

View File

@@ -0,0 +1,170 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 查询分账回退结果
*/
public class QueryReturnOrder {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/profitsharing/return-orders/{out_return_no}";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
QueryReturnOrder client = new QueryReturnOrder(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
QueryReturnOrderRequest request = new QueryReturnOrderRequest();
request.outReturnNo = "R20190516001";
request.outOrderNo = "P20190806125346";
try {
ReturnOrdersEntity response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public ReturnOrdersEntity run(QueryReturnOrderRequest request) {
String uri = PATH;
uri = uri.replace("{out_return_no}", WXPayUtility.urlEncode(request.outReturnNo));
Map<String, Object> args = new HashMap<>();
args.put("out_order_no", request.outOrderNo);
String queryString = WXPayUtility.urlEncode(args);
if (!queryString.isEmpty()) {
uri = uri + "?" + queryString;
}
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, ReturnOrdersEntity.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public QueryReturnOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class QueryReturnOrderRequest {
@SerializedName("out_return_no")
@Expose(serialize = false)
public String outReturnNo;
@SerializedName("out_order_no")
@Expose(serialize = false)
public String outOrderNo;
}
public static class ReturnOrdersEntity {
@SerializedName("order_id")
public String orderId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("out_return_no")
public String outReturnNo;
@SerializedName("return_id")
public String returnId;
@SerializedName("return_mchid")
public String returnMchid;
@SerializedName("amount")
public Long amount;
@SerializedName("description")
public String description;
@SerializedName("result")
public ReturnOrderStatus result;
@SerializedName("fail_reason")
public ReturnOrderFailReason failReason;
@SerializedName("create_time")
public String createTime;
@SerializedName("finish_time")
public String finishTime;
}
public enum ReturnOrderStatus {
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("FAILED")
FAILED
}
public enum ReturnOrderFailReason {
@SerializedName("ACCOUNT_ABNORMAL")
ACCOUNT_ABNORMAL,
@SerializedName("BALANCE_NOT_ENOUGH")
BALANCE_NOT_ENOUGH,
@SerializedName("TIME_OUT_CLOSED")
TIME_OUT_CLOSED,
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
PAYER_ACCOUNT_ABNORMAL,
@SerializedName("INVALID_REQUEST")
INVALID_REQUEST
}
}

View File

@@ -0,0 +1,134 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 申请分账账单
*/
public class SplitBill {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/profitsharing/bills";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
SplitBill client = new SplitBill(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
SplitBillRequest request = new SplitBillRequest();
request.billDate = "2019-06-11";
request.tarType = SplitBillTarType.GZIP;
try {
SplitBillResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public SplitBillResponse run(SplitBillRequest request) {
String uri = PATH;
Map<String, Object> args = new HashMap<>();
args.put("bill_date", request.billDate);
args.put("tar_type", request.tarType);
String queryString = WXPayUtility.urlEncode(args);
if (!queryString.isEmpty()) {
uri = uri + "?" + queryString;
}
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, SplitBillResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public SplitBill(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class SplitBillRequest {
@SerializedName("bill_date")
@Expose(serialize = false)
public String billDate;
@SerializedName("tar_type")
@Expose(serialize = false)
public SplitBillTarType tarType;
}
public static class SplitBillResponse {
@SerializedName("hash_type")
public SplitBillHashType hashType;
@SerializedName("hash_value")
public String hashValue;
@SerializedName("download_url")
public String downloadUrl;
}
public enum SplitBillTarType {
@SerializedName("GZIP")
GZIP
}
public enum SplitBillHashType {
@SerializedName("SHA1")
SHA1
}
}

View File

@@ -0,0 +1,199 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 解冻剩余资金
*/
public class UnfreezeOrder {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/profitsharing/orders/unfreeze";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
UnfreezeOrder client = new UnfreezeOrder(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
UnfreezeOrderRequest request = new UnfreezeOrderRequest();
request.transactionId = "4208450740201411110007820472";
request.outOrderNo = "P20150806125346";
request.description = "解冻全部剩余资金";
try {
OrdersEntity response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public OrdersEntity run(UnfreezeOrderRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, OrdersEntity.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public UnfreezeOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class UnfreezeOrderRequest {
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("description")
public String description;
}
public static class OrdersEntity {
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_order_no")
public String outOrderNo;
@SerializedName("order_id")
public String orderId;
@SerializedName("state")
public OrderStatus state;
@SerializedName("receivers")
public List<OrderReceiverDetail> receivers = new ArrayList<OrderReceiverDetail>();
}
public enum OrderStatus {
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("FINISHED")
FINISHED
}
public static class OrderReceiverDetail {
@SerializedName("amount")
public Long amount;
@SerializedName("description")
public String description;
@SerializedName("type")
public ReceiverType type;
@SerializedName("account")
public String account;
@SerializedName("result")
public DetailStatus result;
@SerializedName("fail_reason")
public DetailFailReason failReason;
@SerializedName("create_time")
public String createTime;
@SerializedName("finish_time")
public String finishTime;
@SerializedName("detail_id")
public String detailId;
}
public enum ReceiverType {
@SerializedName("MERCHANT_ID")
MERCHANT_ID,
@SerializedName("PERSONAL_OPENID")
PERSONAL_OPENID
}
public enum DetailStatus {
@SerializedName("PENDING")
PENDING,
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("CLOSED")
CLOSED
}
public enum DetailFailReason {
@SerializedName("ACCOUNT_ABNORMAL")
ACCOUNT_ABNORMAL,
@SerializedName("NO_RELATION")
NO_RELATION,
@SerializedName("RECEIVER_HIGH_RISK")
RECEIVER_HIGH_RISK,
@SerializedName("RECEIVER_REAL_NAME_NOT_VERIFIED")
RECEIVER_REAL_NAME_NOT_VERIFIED,
@SerializedName("NO_AUTH")
NO_AUTH,
@SerializedName("RECEIVER_RECEIPT_LIMIT")
RECEIVER_RECEIPT_LIMIT,
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
PAYER_ACCOUNT_ABNORMAL,
@SerializedName("INVALID_REQUEST")
INVALID_REQUEST
}
}

View File

@@ -0,0 +1,75 @@
# APP调起支付
> 参考官方文档https://pay.weixin.qq.com/doc/v3/merchant/4013070351
## 说明
商户通过APP下单接口获取到发起支付的必要参数 `prepay_id`商户APP再通过 OpenSDK 的 `sendReq` 方法拉起微信支付。
**注意**:需严格遵循 OpenSDK 接入指引([Android](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/Android.html) / [iOS](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html) / [鸿蒙](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/ohos.html)接入SDK以及配置开发环境。
## 请求参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `appId` | 是 | 下单时传入的应用ID |
| `partnerId` | 是 | 下单时传入的商户号 |
| `prepayId` | 是 | APP下单接口返回的 prepay_id有效期2小时 |
| `packageValue` | 是 | 固定值 `Sign=WXPay`iOS 中字段名为 `package` |
| `nonceStr` | 是 | 随机字符串不长于32位 |
| `timeStamp` | 是 | Unix 秒级时间戳 |
| `sign` | 是 | 使用 appId、timeStamp、nonceStr、prepayId + 商户API证书私钥生成的 RSA 签名 |
## iOS 示例代码
```objectivec
PayReq *request = [[[PayReq alloc] init] autorelease];
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.package = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==";
[WXApi sendReqrequest];
```
## Android 示例代码
```java
IWXAPI api;
PayReq request = new PayReq();
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.packageValue = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==";
api.sendReq(request);
```
## 鸿蒙示例代码
```typescript
IWXAPI api;
let req = new wxopensdk.PayReq
req.appId = 'wxd930ea5d5a258f4f'
req.partnerId = '1900000109'
req.prepayId = '1101000000140415649af9fc314aa427'
req.packageValue = 'Sign=WXPay'
req.nonceStr = '1101000000140429eb40476f8896f4c9'
req.timeStamp = '1398746574'
req.sign = 'oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg=='
api.sendReq(context: common.UIAbilityContext, req)
```
## 回调 errCode 说明
| errCode | 描述 | 商户APP处理方案 |
|---------|------|---------------|
| 0 | 成功 | 调用后端查单接口,订单已支付则展示支付成功页面 |
| -1 | 错误 | 可能原因签名错误、未注册AppID、AppID不匹配等 |
| -2 | 取消支付 | 用户取消支付返回App商户自行处理展示 |
> **重要**:前端回调不保证绝对可靠,不可只依赖前端回调判断订单支付状态,订单状态需以后端查询订单和支付成功回调通知为准。

View File

@@ -0,0 +1,244 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* App下单
*/
public class AppPrepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/pay/transactions/app";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
AppPrepay client = new AppPrepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
CommonPrepayRequest request = new CommonPrepayRequest();
request.appid = "wxd678efh567hg6787";
request.mchid = "1230000109";
request.description = "Image形象店-深圳腾大-QQ公仔";
request.outTradeNo = "1217752501201407033233368018";
request.timeExpire = "2018-06-08T10:34:56+08:00";
request.attach = "自定义数据说明";
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
request.goodsTag = "WXG";
request.supportFapiao = false;
request.amount = new CommonAmountInfo();
request.amount.total = 100L;
request.amount.currency = "CNY";
request.detail = new CouponInfo();
request.detail.costPrice = 608800L;
request.detail.invoiceId = "微信123";
request.detail.goodsDetail = new ArrayList<>();
{
GoodsDetail goodsDetailItem = new GoodsDetail();
goodsDetailItem.merchantGoodsId = "1246464644";
goodsDetailItem.wechatpayGoodsId = "1001";
goodsDetailItem.goodsName = "iPhoneX 256G";
goodsDetailItem.quantity = 1L;
goodsDetailItem.unitPrice = 528800L;
request.detail.goodsDetail.add(goodsDetailItem);
};
request.sceneInfo = new CommonSceneInfo();
request.sceneInfo.payerClientIp = "14.23.150.211";
request.sceneInfo.deviceId = "013467007045764";
request.sceneInfo.storeInfo = new StoreInfo();
request.sceneInfo.storeInfo.id = "0001";
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
request.sceneInfo.storeInfo.areaCode = "440305";
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
request.settleInfo = new SettleInfo();
request.settleInfo.profitSharing = false;
try {
DirectAPIv3AppPrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public DirectAPIv3AppPrepayResponse run(CommonPrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, DirectAPIv3AppPrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public AppPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class CommonPrepayRequest {
@SerializedName("appid")
public String appid;
@SerializedName("mchid")
public String mchid;
@SerializedName("description")
public String description;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("attach")
public String attach;
@SerializedName("notify_url")
public String notifyUrl;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("support_fapiao")
public Boolean supportFapiao;
@SerializedName("amount")
public CommonAmountInfo amount;
@SerializedName("detail")
public CouponInfo detail;
@SerializedName("scene_info")
public CommonSceneInfo sceneInfo;
@SerializedName("settle_info")
public SettleInfo settleInfo;
}
public static class DirectAPIv3AppPrepayResponse {
@SerializedName("prepay_id")
public String prepayId;
}
public static class CommonAmountInfo {
@SerializedName("total")
public Long total;
@SerializedName("currency")
public String currency;
}
public static class CouponInfo {
@SerializedName("cost_price")
public Long costPrice;
@SerializedName("invoice_id")
public String invoiceId;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class CommonSceneInfo {
@SerializedName("payer_client_ip")
public String payerClientIp;
@SerializedName("device_id")
public String deviceId;
@SerializedName("store_info")
public StoreInfo storeInfo;
}
public static class SettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
public static class GoodsDetail {
@SerializedName("merchant_goods_id")
public String merchantGoodsId;
@SerializedName("wechatpay_goods_id")
public String wechatpayGoodsId;
@SerializedName("goods_name")
public String goodsName;
@SerializedName("quantity")
public Long quantity;
@SerializedName("unit_price")
public Long unitPrice;
}
public static class StoreInfo {
@SerializedName("id")
public String id;
@SerializedName("name")
public String name;
@SerializedName("area_code")
public String areaCode;
@SerializedName("address")
public String address;
}
}

View File

@@ -0,0 +1,29 @@
# H5调起支付
> 参考官方文档https://pay.weixin.qq.com/doc/v3/merchant/4012791835
## 调起支付步骤
1. 商户通过H5下单接口获取到发起支付的必要参数 `h5_url`
2. 商户在配置了H5支付域名的网页中跳转 `h5_url`,调起微信支付收银台中间页
3. 微信支付收银台中间页会进行H5权限的校验和安全性检查校验通过后用户可正常支付
## 支付后返回指定页面
用户支付完成后默认返回发起支付的页面。如需返回指定页面,可在 `h5_url` 后拼接 `redirect_url` 参数:
```
h5_url=https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https%3A%2F%2Fwww.wechatpay.com.cn
```
**注意**
- `redirect_url` 的域名必须为商户配置的H5支付域名
- 需对 `redirect_url` 进行 urlencode 处理
## 返回商户页面后查单
用户点击"取消支付"或支付成功后点击"完成"时,会返回商户支付页面(或指定的 redirect_url 页面)。建议:
在回跳页面中增设"确认支付情况"按钮,用户点击后触发查单操作,确保用户能及时了解订单状态。
> **重要**H5支付不像JSAPI/APP有前端回调商户必须通过后端查单接口或支付成功回调通知来确认订单状态。

View File

@@ -0,0 +1,270 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* H5下单
*/
public class H5Prepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/pay/transactions/h5";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
H5Prepay client = new H5Prepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
DirectAPIv3H5PrepayRequest request = new DirectAPIv3H5PrepayRequest();
request.appid = "wxd678efh567hg6787";
request.mchid = "1230000109";
request.description = "Image形象店-深圳腾大-QQ公仔";
request.outTradeNo = "1217752501201407033233368018";
request.timeExpire = "2018-06-08T10:34:56+08:00";
request.attach = "自定义数据说明";
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
request.goodsTag = "WXG";
request.supportFapiao = false;
request.amount = new CommonAmountInfo();
request.amount.total = 100L;
request.amount.currency = "CNY";
request.detail = new CouponInfo();
request.detail.costPrice = 608800L;
request.detail.invoiceId = "微信123";
request.detail.goodsDetail = new ArrayList<>();
{
GoodsDetail goodsDetailItem = new GoodsDetail();
goodsDetailItem.merchantGoodsId = "1246464644";
goodsDetailItem.wechatpayGoodsId = "1001";
goodsDetailItem.goodsName = "iPhoneX 256G";
goodsDetailItem.quantity = 1L;
goodsDetailItem.unitPrice = 528800L;
request.detail.goodsDetail.add(goodsDetailItem);
};
request.sceneInfo = new H5ReqSceneInfo();
request.sceneInfo.payerClientIp = "14.23.150.211";
request.sceneInfo.deviceId = "013467007045764";
request.sceneInfo.storeInfo = new StoreInfo();
request.sceneInfo.storeInfo.id = "0001";
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
request.sceneInfo.storeInfo.areaCode = "440305";
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
request.sceneInfo.h5Info = new H5Info();
request.sceneInfo.h5Info.type = "iOS";
request.sceneInfo.h5Info.appName = "王者荣耀";
request.sceneInfo.h5Info.appUrl = "https://pay.qq.com";
request.sceneInfo.h5Info.bundleId = "com.tencent.wzryiOS";
request.sceneInfo.h5Info.packageName = "com.tencent.tmgp.sgame";
request.settleInfo = new SettleInfo();
request.settleInfo.profitSharing = false;
try {
DirectAPIv3H5PrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public DirectAPIv3H5PrepayResponse run(DirectAPIv3H5PrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, DirectAPIv3H5PrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public H5Prepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class DirectAPIv3H5PrepayRequest {
@SerializedName("appid")
public String appid;
@SerializedName("mchid")
public String mchid;
@SerializedName("description")
public String description;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("attach")
public String attach;
@SerializedName("notify_url")
public String notifyUrl;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("support_fapiao")
public Boolean supportFapiao;
@SerializedName("amount")
public CommonAmountInfo amount;
@SerializedName("detail")
public CouponInfo detail;
@SerializedName("scene_info")
public H5ReqSceneInfo sceneInfo;
@SerializedName("settle_info")
public SettleInfo settleInfo;
}
public static class DirectAPIv3H5PrepayResponse {
@SerializedName("h5_url")
public String h5Url;
}
public static class CommonAmountInfo {
@SerializedName("total")
public Long total;
@SerializedName("currency")
public String currency;
}
public static class CouponInfo {
@SerializedName("cost_price")
public Long costPrice;
@SerializedName("invoice_id")
public String invoiceId;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class H5ReqSceneInfo {
@SerializedName("payer_client_ip")
public String payerClientIp;
@SerializedName("device_id")
public String deviceId;
@SerializedName("store_info")
public StoreInfo storeInfo;
@SerializedName("h5_info")
public H5Info h5Info;
}
public static class SettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
public static class GoodsDetail {
@SerializedName("merchant_goods_id")
public String merchantGoodsId;
@SerializedName("wechatpay_goods_id")
public String wechatpayGoodsId;
@SerializedName("goods_name")
public String goodsName;
@SerializedName("quantity")
public Long quantity;
@SerializedName("unit_price")
public Long unitPrice;
}
public static class StoreInfo {
@SerializedName("id")
public String id;
@SerializedName("name")
public String name;
@SerializedName("area_code")
public String areaCode;
@SerializedName("address")
public String address;
}
public static class H5Info {
@SerializedName("type")
public String type;
@SerializedName("app_name")
public String appName;
@SerializedName("app_url")
public String appUrl;
@SerializedName("bundle_id")
public String bundleId;
@SerializedName("package_name")
public String packageName;
}
}

View File

@@ -0,0 +1,19 @@
# Native调起支付
> 参考官方文档https://pay.weixin.qq.com/doc/v3/merchant/4012791878
## 调起支付步骤
1. 通过 Native下单接口获取到发起支付的必要参数 `code_url`
2.`code_url` 链接转换为二维码图片后,展示给用户
3. 用户打开微信"扫一扫"功能,扫描二维码,进行 Native 支付
## 示例
`weixin://pay.weixin.qq.com/bizpayurl/up?pr=NwY5Mz9&groupid=00` 生成二维码,展示给用户扫码即可。
## 注意事项
- Native 支付的 `code_url` 有效期为2小时超时需重新调用下单接口获取新的 `code_url`
- 二维码仅支持微信"扫一扫"功能扫描,不支持长按识别或相册识别
- 商户需通过后端查单接口或支付成功回调通知来确认订单状态,不能仅依赖用户告知

View File

@@ -0,0 +1,244 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Native下单
*/
public class NativePrepay {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/pay/transactions/native";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
NativePrepay client = new NativePrepay(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
CommonPrepayRequest request = new CommonPrepayRequest();
request.appid = "wxd678efh567hg6787";
request.mchid = "1230000109";
request.description = "Image形象店-深圳腾大-QQ公仔";
request.outTradeNo = "1217752501201407033233368018";
request.timeExpire = "2018-06-08T10:34:56+08:00";
request.attach = "自定义数据说明";
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
request.goodsTag = "WXG";
request.supportFapiao = false;
request.amount = new CommonAmountInfo();
request.amount.total = 100L;
request.amount.currency = "CNY";
request.detail = new CouponInfo();
request.detail.costPrice = 608800L;
request.detail.invoiceId = "微信123";
request.detail.goodsDetail = new ArrayList<>();
{
GoodsDetail goodsDetailItem = new GoodsDetail();
goodsDetailItem.merchantGoodsId = "1246464644";
goodsDetailItem.wechatpayGoodsId = "1001";
goodsDetailItem.goodsName = "iPhoneX 256G";
goodsDetailItem.quantity = 1L;
goodsDetailItem.unitPrice = 528800L;
request.detail.goodsDetail.add(goodsDetailItem);
};
request.sceneInfo = new CommonSceneInfo();
request.sceneInfo.payerClientIp = "14.23.150.211";
request.sceneInfo.deviceId = "013467007045764";
request.sceneInfo.storeInfo = new StoreInfo();
request.sceneInfo.storeInfo.id = "0001";
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
request.sceneInfo.storeInfo.areaCode = "440305";
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
request.settleInfo = new SettleInfo();
request.settleInfo.profitSharing = false;
try {
DirectAPIv3DirectNativePrepayResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public DirectAPIv3DirectNativePrepayResponse run(CommonPrepayRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, DirectAPIv3DirectNativePrepayResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public NativePrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class CommonPrepayRequest {
@SerializedName("appid")
public String appid;
@SerializedName("mchid")
public String mchid;
@SerializedName("description")
public String description;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("time_expire")
public String timeExpire;
@SerializedName("attach")
public String attach;
@SerializedName("notify_url")
public String notifyUrl;
@SerializedName("goods_tag")
public String goodsTag;
@SerializedName("support_fapiao")
public Boolean supportFapiao;
@SerializedName("amount")
public CommonAmountInfo amount;
@SerializedName("detail")
public CouponInfo detail;
@SerializedName("scene_info")
public CommonSceneInfo sceneInfo;
@SerializedName("settle_info")
public SettleInfo settleInfo;
}
public static class DirectAPIv3DirectNativePrepayResponse {
@SerializedName("code_url")
public String codeUrl;
}
public static class CommonAmountInfo {
@SerializedName("total")
public Long total;
@SerializedName("currency")
public String currency;
}
public static class CouponInfo {
@SerializedName("cost_price")
public Long costPrice;
@SerializedName("invoice_id")
public String invoiceId;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class CommonSceneInfo {
@SerializedName("payer_client_ip")
public String payerClientIp;
@SerializedName("device_id")
public String deviceId;
@SerializedName("store_info")
public StoreInfo storeInfo;
}
public static class SettleInfo {
@SerializedName("profit_sharing")
public Boolean profitSharing;
}
public static class GoodsDetail {
@SerializedName("merchant_goods_id")
public String merchantGoodsId;
@SerializedName("wechatpay_goods_id")
public String wechatpayGoodsId;
@SerializedName("goods_name")
public String goodsName;
@SerializedName("quantity")
public Long quantity;
@SerializedName("unit_price")
public Long unitPrice;
}
public static class StoreInfo {
@SerializedName("id")
public String id;
@SerializedName("name")
public String name;
@SerializedName("area_code")
public String areaCode;
@SerializedName("address")
public String address;
}
}

View File

@@ -0,0 +1,26 @@
# 小程序调起支付
## 说明
商户通过JSAPI/小程序下单接口获取到发起支付的必要参数 `prepay_id` 后,在小程序中通过 `wx.requestPayment` 调起微信支付收银台。
小程序和JSAPI共用同一个下单接口`/v3/pay/transactions/jsapi`),区别在于调起支付的前端方式不同:小程序用 `wx.requestPayment`JSAPI 用 `WeixinJSBridge`
## 示例代码
```javascript
wx.requestPayment(
{
"timeStamp": "1414561699",
"nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
"package": "prepay_id=wx201410272009395522657a690389285100",
"signType": "RSA",
"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==",
"success":function(res){},
"fail":function(res){},
"complete":function(res){}
}
)
```
> **重要**:前端回调不保证绝对可靠,商户需通过后端查单接口或支付成功回调通知来确认订单状态。

View File

@@ -0,0 +1,233 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 微信支付商户订单号查询订单
*/
public class QueryByOutTradeNo {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/pay/transactions/out-trade-no/{out_trade_no}";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
QueryByOutTradeNo client = new QueryByOutTradeNo(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
QueryByOutTradeNoRequest request = new QueryByOutTradeNoRequest();
request.outTradeNo = "1217752501201407033233368018";
request.mchid = "1230000109";
try {
DirectAPIv3QueryResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public DirectAPIv3QueryResponse run(QueryByOutTradeNoRequest request) {
String uri = PATH;
uri = uri.replace("{out_trade_no}", WXPayUtility.urlEncode(request.outTradeNo));
Map<String, Object> args = new HashMap<>();
args.put("mchid", request.mchid);
String queryString = WXPayUtility.urlEncode(args);
if (!queryString.isEmpty()) {
uri = uri + "?" + queryString;
}
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, DirectAPIv3QueryResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public QueryByOutTradeNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class QueryByOutTradeNoRequest {
@SerializedName("mchid")
@Expose(serialize = false)
public String mchid;
@SerializedName("out_trade_no")
@Expose(serialize = false)
public String outTradeNo;
}
public static class DirectAPIv3QueryResponse {
@SerializedName("appid")
public String appid;
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("trade_type")
public String tradeType;
@SerializedName("trade_state")
public String tradeState;
@SerializedName("trade_state_desc")
public String tradeStateDesc;
@SerializedName("bank_type")
public String bankType;
@SerializedName("attach")
public String attach;
@SerializedName("success_time")
public String successTime;
@SerializedName("payer")
public CommRespPayerInfo payer;
@SerializedName("amount")
public CommRespAmountInfo amount;
@SerializedName("scene_info")
public CommRespSceneInfo sceneInfo;
@SerializedName("promotion_detail")
public List<PromotionDetail> promotionDetail;
}
public static class CommRespPayerInfo {
@SerializedName("openid")
public String openid;
}
public static class CommRespAmountInfo {
@SerializedName("total")
public Long total;
@SerializedName("payer_total")
public Long payerTotal;
@SerializedName("currency")
public String currency;
@SerializedName("payer_currency")
public String payerCurrency;
}
public static class CommRespSceneInfo {
@SerializedName("device_id")
public String deviceId;
}
public static class PromotionDetail {
@SerializedName("coupon_id")
public String couponId;
@SerializedName("name")
public String name;
@SerializedName("scope")
public String scope;
@SerializedName("type")
public String type;
@SerializedName("amount")
public Long amount;
@SerializedName("stock_id")
public String stockId;
@SerializedName("wechatpay_contribute")
public Long wechatpayContribute;
@SerializedName("merchant_contribute")
public Long merchantContribute;
@SerializedName("other_contribute")
public Long otherContribute;
@SerializedName("currency")
public String currency;
@SerializedName("goods_detail")
public List<GoodsDetailInPromotion> goodsDetail;
}
public static class GoodsDetailInPromotion {
@SerializedName("goods_id")
public String goodsId;
@SerializedName("quantity")
public Long quantity;
@SerializedName("unit_price")
public Long unitPrice;
@SerializedName("discount_amount")
public Long discountAmount;
@SerializedName("goods_remark")
public String goodsRemark;
}
}

View File

@@ -0,0 +1,233 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 微信支付订单号查询订单
*/
public class QueryByWxTradeNo {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/pay/transactions/id/{transaction_id}";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
QueryByWxTradeNo client = new QueryByWxTradeNo(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
QueryByWxTradeNoRequest request = new QueryByWxTradeNoRequest();
request.transactionId = "1217752501201407033233368018";
request.mchid = "1230000109";
try {
DirectAPIv3QueryResponse response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public DirectAPIv3QueryResponse run(QueryByWxTradeNoRequest request) {
String uri = PATH;
uri = uri.replace("{transaction_id}", WXPayUtility.urlEncode(request.transactionId));
Map<String, Object> args = new HashMap<>();
args.put("mchid", request.mchid);
String queryString = WXPayUtility.urlEncode(args);
if (!queryString.isEmpty()) {
uri = uri + "?" + queryString;
}
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, DirectAPIv3QueryResponse.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public QueryByWxTradeNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class QueryByWxTradeNoRequest {
@SerializedName("mchid")
@Expose(serialize = false)
public String mchid;
@SerializedName("transaction_id")
@Expose(serialize = false)
public String transactionId;
}
public static class DirectAPIv3QueryResponse {
@SerializedName("appid")
public String appid;
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("trade_type")
public String tradeType;
@SerializedName("trade_state")
public String tradeState;
@SerializedName("trade_state_desc")
public String tradeStateDesc;
@SerializedName("bank_type")
public String bankType;
@SerializedName("attach")
public String attach;
@SerializedName("success_time")
public String successTime;
@SerializedName("payer")
public CommRespPayerInfo payer;
@SerializedName("amount")
public CommRespAmountInfo amount;
@SerializedName("scene_info")
public CommRespSceneInfo sceneInfo;
@SerializedName("promotion_detail")
public List<PromotionDetail> promotionDetail;
}
public static class CommRespPayerInfo {
@SerializedName("openid")
public String openid;
}
public static class CommRespAmountInfo {
@SerializedName("total")
public Long total;
@SerializedName("payer_total")
public Long payerTotal;
@SerializedName("currency")
public String currency;
@SerializedName("payer_currency")
public String payerCurrency;
}
public static class CommRespSceneInfo {
@SerializedName("device_id")
public String deviceId;
}
public static class PromotionDetail {
@SerializedName("coupon_id")
public String couponId;
@SerializedName("name")
public String name;
@SerializedName("scope")
public String scope;
@SerializedName("type")
public String type;
@SerializedName("amount")
public Long amount;
@SerializedName("stock_id")
public String stockId;
@SerializedName("wechatpay_contribute")
public Long wechatpayContribute;
@SerializedName("merchant_contribute")
public Long merchantContribute;
@SerializedName("other_contribute")
public Long otherContribute;
@SerializedName("currency")
public String currency;
@SerializedName("goods_detail")
public List<GoodsDetailInPromotion> goodsDetail;
}
public static class GoodsDetailInPromotion {
@SerializedName("goods_id")
public String goodsId;
@SerializedName("quantity")
public Long quantity;
@SerializedName("unit_price")
public Long unitPrice;
@SerializedName("discount_amount")
public Long discountAmount;
@SerializedName("goods_remark")
public String goodsRemark;
}
}

View File

@@ -0,0 +1,110 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 关闭订单
*
* 未支付状态的订单,可在无需支付时调用此接口关闭订单。常见关单情况包括:
* 1. 用户在商户系统提交取消订单请求,商户需执行关单操作。
* 2. 订单超时未支付超出商户系统设定的可支付时间或下单时的time_expire支付截止时间商户需进行关单处理。
*/
public class CloseOrder {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/pay/transactions/out-trade-no/{out_trade_no}/close";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
CloseOrder client = new CloseOrder(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
CloseOrderRequest request = new CloseOrderRequest();
request.outTradeNo = "1217752501201407033233368018";
request.mchid = "1230000109";
try {
client.run(request);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public void run(CloseOrderRequest request) {
String uri = PATH;
uri = uri.replace("{out_trade_no}", WXPayUtility.urlEncode(request.outTradeNo));
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
return;
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public CloseOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class CloseOrderRequest {
@SerializedName("mchid")
public String mchid;
@SerializedName("out_trade_no")
@Expose(serialize = false)
public String outTradeNo;
}
}

View File

@@ -0,0 +1,314 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 发起异常退款
*/
public class CreateAbnormalRefund {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
CreateAbnormalRefund client = new CreateAbnormalRefund(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
CreateAbnormalRefundRequest request = new CreateAbnormalRefundRequest();
request.refundId = "50000000382019052709732678859";
request.outRefundNo = "1217752501201407033233368018";
request.type = AbnormalReceiveType.MERCHANT_BANK_CARD;
request.bankType = "ICBC_DEBIT";
request.bankAccount = client.encrypt("bank_account");
request.realName = client.encrypt("real_name");
try {
Refund response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public Refund run(CreateAbnormalRefundRequest request) {
String uri = PATH;
uri = uri.replace("{refund_id}", WXPayUtility.urlEncode(request.refundId));
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, Refund.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public CreateAbnormalRefund(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public String encrypt(String plainText) {
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
}
public static class CreateAbnormalRefundRequest {
@SerializedName("refund_id")
@Expose(serialize = false)
public String refundId;
@SerializedName("out_refund_no")
public String outRefundNo;
@SerializedName("type")
public AbnormalReceiveType type;
@SerializedName("bank_type")
public String bankType;
@SerializedName("bank_account")
public String bankAccount;
@SerializedName("real_name")
public String realName;
}
public static class Refund {
@SerializedName("refund_id")
public String refundId;
@SerializedName("out_refund_no")
public String outRefundNo;
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("channel")
public Channel channel;
@SerializedName("user_received_account")
public String userReceivedAccount;
@SerializedName("success_time")
public String successTime;
@SerializedName("create_time")
public String createTime;
@SerializedName("status")
public Status status;
@SerializedName("funds_account")
public FundsAccount fundsAccount;
@SerializedName("amount")
public Amount amount;
@SerializedName("promotion_detail")
public List<Promotion> promotionDetail;
}
public enum AbnormalReceiveType {
@SerializedName("USER_BANK_CARD")
USER_BANK_CARD,
@SerializedName("MERCHANT_BANK_CARD")
MERCHANT_BANK_CARD
}
public enum Channel {
@SerializedName("ORIGINAL")
ORIGINAL,
@SerializedName("BALANCE")
BALANCE,
@SerializedName("OTHER_BALANCE")
OTHER_BALANCE,
@SerializedName("OTHER_BANKCARD")
OTHER_BANKCARD
}
public enum Status {
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("CLOSED")
CLOSED,
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("ABNORMAL")
ABNORMAL
}
public enum FundsAccount {
@SerializedName("UNSETTLED")
UNSETTLED,
@SerializedName("AVAILABLE")
AVAILABLE,
@SerializedName("UNAVAILABLE")
UNAVAILABLE,
@SerializedName("OPERATION")
OPERATION,
@SerializedName("BASIC")
BASIC,
@SerializedName("ECNY_BASIC")
ECNY_BASIC
}
public static class Amount {
@SerializedName("total")
public Long total;
@SerializedName("refund")
public Long refund;
@SerializedName("from")
public List<FundsFromItem> from;
@SerializedName("payer_total")
public Long payerTotal;
@SerializedName("payer_refund")
public Long payerRefund;
@SerializedName("settlement_refund")
public Long settlementRefund;
@SerializedName("settlement_total")
public Long settlementTotal;
@SerializedName("discount_refund")
public Long discountRefund;
@SerializedName("currency")
public String currency;
@SerializedName("refund_fee")
public Long refundFee;
}
public static class Promotion {
@SerializedName("promotion_id")
public String promotionId;
@SerializedName("scope")
public PromotionScope scope;
@SerializedName("type")
public PromotionType type;
@SerializedName("amount")
public Long amount;
@SerializedName("refund_amount")
public Long refundAmount;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class FundsFromItem {
@SerializedName("account")
public Account account;
@SerializedName("amount")
public Long amount;
}
public enum PromotionScope {
@SerializedName("GLOBAL")
GLOBAL,
@SerializedName("SINGLE")
SINGLE
}
public enum PromotionType {
@SerializedName("CASH")
CASH,
@SerializedName("NOCASH")
NOCASH
}
public static class GoodsDetail {
@SerializedName("merchant_goods_id")
public String merchantGoodsId;
@SerializedName("wechatpay_goods_id")
public String wechatpayGoodsId;
@SerializedName("goods_name")
public String goodsName;
@SerializedName("unit_price")
public Long unitPrice;
@SerializedName("refund_amount")
public Long refundAmount;
@SerializedName("refund_quantity")
public Long refundQuantity;
}
public enum Account {
@SerializedName("AVAILABLE")
AVAILABLE,
@SerializedName("UNAVAILABLE")
UNAVAILABLE
}
}

View File

@@ -0,0 +1,360 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 退款申请
*
* 支付成功后1年内可通过此接口将款项全部或部分原路退还给用户也可在商户平台手动操作
*
* 关键注意:
* 1. 一笔订单最多50次部分退款重试必须用原 out_refund_no否则会重复退款。
* 2. 接口返回成功仅表示受理成功,实际结果以退款回调通知或查询退款接口为准。
* 3. 原路退还银行卡1-3个工作日到账零钱即时到账。
* 4. 有代金券的订单部分退款时,退给用户 = 退款金额 × (实付 ÷ 总额),四舍五入。
* 5. 有分账的订单,需确保可用余额充足;部分分账未解冻时需先调"完结分账"。
*/
public class CreateRefund {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "POST";
private static String PATH = "/v3/refund/domestic/refunds";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
CreateRefund client = new CreateRefund(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
CreateRequest request = new CreateRequest();
request.transactionId = "1217752501201407033233368018";
request.outTradeNo = "1217752501201407033233368018";
request.outRefundNo = "1217752501201407033233368018";
request.reason = "商品已售完";
request.notifyUrl = "https://weixin.qq.com";
request.fundsAccount = ReqFundsAccount.AVAILABLE;
request.amount = new AmountReq();
request.amount.refund = 888L;
request.amount.from = new ArrayList<>();
{
FundsFromItem fromItem = new FundsFromItem();
fromItem.account = Account.AVAILABLE;
fromItem.amount = 444L;
request.amount.from.add(fromItem);
};
request.amount.total = 888L;
request.amount.currency = "CNY";
request.goodsDetail = new ArrayList<>();
{
GoodsDetail goodsDetailItem = new GoodsDetail();
goodsDetailItem.merchantGoodsId = "1217752501201407033233368018";
goodsDetailItem.wechatpayGoodsId = "1001";
goodsDetailItem.goodsName = "iPhone6s 16G";
goodsDetailItem.unitPrice = 528800L;
goodsDetailItem.refundAmount = 528800L;
goodsDetailItem.refundQuantity = 1L;
request.goodsDetail.add(goodsDetailItem);
};
try {
Refund response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public Refund run(CreateRequest request) {
String uri = PATH;
String reqBody = WXPayUtility.toJson(request);
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
reqBuilder.addHeader("Content-Type", "application/json");
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
reqBuilder.method(METHOD, requestBody);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, Refund.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public CreateRefund(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class CreateRequest {
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("out_refund_no")
public String outRefundNo;
@SerializedName("reason")
public String reason;
@SerializedName("notify_url")
public String notifyUrl;
@SerializedName("funds_account")
public ReqFundsAccount fundsAccount;
@SerializedName("amount")
public AmountReq amount;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class Refund {
@SerializedName("refund_id")
public String refundId;
@SerializedName("out_refund_no")
public String outRefundNo;
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("channel")
public Channel channel;
@SerializedName("user_received_account")
public String userReceivedAccount;
@SerializedName("success_time")
public String successTime;
@SerializedName("create_time")
public String createTime;
@SerializedName("status")
public Status status;
@SerializedName("funds_account")
public FundsAccount fundsAccount;
@SerializedName("amount")
public Amount amount;
@SerializedName("promotion_detail")
public List<Promotion> promotionDetail;
}
public enum ReqFundsAccount {
@SerializedName("AVAILABLE")
AVAILABLE,
@SerializedName("UNSETTLED")
UNSETTLED
}
public static class AmountReq {
@SerializedName("refund")
public Long refund;
@SerializedName("from")
public List<FundsFromItem> from;
@SerializedName("total")
public Long total;
@SerializedName("currency")
public String currency;
}
public static class GoodsDetail {
@SerializedName("merchant_goods_id")
public String merchantGoodsId;
@SerializedName("wechatpay_goods_id")
public String wechatpayGoodsId;
@SerializedName("goods_name")
public String goodsName;
@SerializedName("unit_price")
public Long unitPrice;
@SerializedName("refund_amount")
public Long refundAmount;
@SerializedName("refund_quantity")
public Long refundQuantity;
}
public enum Channel {
@SerializedName("ORIGINAL")
ORIGINAL,
@SerializedName("BALANCE")
BALANCE,
@SerializedName("OTHER_BALANCE")
OTHER_BALANCE,
@SerializedName("OTHER_BANKCARD")
OTHER_BANKCARD
}
public enum Status {
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("CLOSED")
CLOSED,
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("ABNORMAL")
ABNORMAL
}
public enum FundsAccount {
@SerializedName("UNSETTLED")
UNSETTLED,
@SerializedName("AVAILABLE")
AVAILABLE,
@SerializedName("UNAVAILABLE")
UNAVAILABLE,
@SerializedName("OPERATION")
OPERATION,
@SerializedName("BASIC")
BASIC,
@SerializedName("ECNY_BASIC")
ECNY_BASIC
}
public static class Amount {
@SerializedName("total")
public Long total;
@SerializedName("refund")
public Long refund;
@SerializedName("from")
public List<FundsFromItem> from;
@SerializedName("payer_total")
public Long payerTotal;
@SerializedName("payer_refund")
public Long payerRefund;
@SerializedName("settlement_refund")
public Long settlementRefund;
@SerializedName("settlement_total")
public Long settlementTotal;
@SerializedName("discount_refund")
public Long discountRefund;
@SerializedName("currency")
public String currency;
@SerializedName("refund_fee")
public Long refundFee;
}
public static class Promotion {
@SerializedName("promotion_id")
public String promotionId;
@SerializedName("scope")
public PromotionScope scope;
@SerializedName("type")
public PromotionType type;
@SerializedName("amount")
public Long amount;
@SerializedName("refund_amount")
public Long refundAmount;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class FundsFromItem {
@SerializedName("account")
public Account account;
@SerializedName("amount")
public Long amount;
}
public enum PromotionScope {
@SerializedName("GLOBAL")
GLOBAL,
@SerializedName("SINGLE")
SINGLE
}
public enum PromotionType {
@SerializedName("CASH")
CASH,
@SerializedName("NOCASH")
NOCASH
}
public enum Account {
@SerializedName("AVAILABLE")
AVAILABLE,
@SerializedName("UNAVAILABLE")
UNAVAILABLE
}
}

View File

@@ -0,0 +1,286 @@
package com.java.demo;
import com.java.utils.WXPayUtility; // 引用微信支付工具库参考https://pay.weixin.qq.com/doc/v3/merchant/4014931831
import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.Expose;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 查询单笔退款(通过商户退款单号)
*
* 提交退款申请后推荐每间隔1分钟调用该接口查询一次退款状态若超过5分钟仍是退款处理中状态
* 建议开始逐步衰减查询频率(比如之后间隔5分钟、10分钟、20分钟、30分钟……查询一次)。
*
* 退款有一定延时零钱支付的订单退款一般5分钟内到账银行卡支付的订单退款一般1-3个工作日到账。
*
* 同一商户号查询退款频率限制为300qps如返回FREQUENCY_LIMITED频率限制报错可间隔1分钟再重试查询。
*/
public class QueryByOutRefundNo {
private static String HOST = "https://api.mch.weixin.qq.com";
private static String METHOD = "GET";
private static String PATH = "/v3/refund/domestic/refunds/{out_refund_no}";
public static void main(String[] args) {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/merchant/4013070756
QueryByOutRefundNo client = new QueryByOutRefundNo(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
);
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
request.outRefundNo = "1217752501201407033233368018";
try {
Refund response = client.run(request);
// TODO: 请求成功,继续业务逻辑
System.out.println(response);
} catch (WXPayUtility.ApiException e) {
// TODO: 请求失败,根据状态码执行不同的逻辑
e.printStackTrace();
}
}
public Refund run(QueryByOutRefundNoRequest request) {
String uri = PATH;
uri = uri.replace("{out_refund_no}", WXPayUtility.urlEncode(request.outRefundNo));
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
reqBuilder.addHeader("Accept", "application/json");
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
reqBuilder.method(METHOD, null);
Request httpRequest = reqBuilder.build();
// 发送HTTP请求
OkHttpClient client = new OkHttpClient.Builder().build();
try (Response httpResponse = client.newCall(httpRequest).execute()) {
String respBody = WXPayUtility.extractBody(httpResponse);
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
// 2XX 成功,验证应答签名
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
httpResponse.headers(), respBody);
// 从HTTP应答报文构建返回数据
return WXPayUtility.fromJson(respBody, Refund.class);
} else {
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
}
} catch (IOException e) {
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
}
}
private final String mchid;
private final String certificateSerialNo;
private final PrivateKey privateKey;
private final String wechatPayPublicKeyId;
private final PublicKey wechatPayPublicKey;
public QueryByOutRefundNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
this.mchid = mchid;
this.certificateSerialNo = certificateSerialNo;
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
}
public static class QueryByOutRefundNoRequest {
@SerializedName("out_refund_no")
@Expose(serialize = false)
public String outRefundNo;
}
public static class Refund {
@SerializedName("refund_id")
public String refundId;
@SerializedName("out_refund_no")
public String outRefundNo;
@SerializedName("transaction_id")
public String transactionId;
@SerializedName("out_trade_no")
public String outTradeNo;
@SerializedName("channel")
public Channel channel;
@SerializedName("user_received_account")
public String userReceivedAccount;
@SerializedName("success_time")
public String successTime;
@SerializedName("create_time")
public String createTime;
@SerializedName("status")
public Status status;
@SerializedName("funds_account")
public FundsAccount fundsAccount;
@SerializedName("amount")
public Amount amount;
@SerializedName("promotion_detail")
public List<Promotion> promotionDetail;
}
public enum Channel {
@SerializedName("ORIGINAL")
ORIGINAL,
@SerializedName("BALANCE")
BALANCE,
@SerializedName("OTHER_BALANCE")
OTHER_BALANCE,
@SerializedName("OTHER_BANKCARD")
OTHER_BANKCARD
}
public enum Status {
@SerializedName("SUCCESS")
SUCCESS,
@SerializedName("CLOSED")
CLOSED,
@SerializedName("PROCESSING")
PROCESSING,
@SerializedName("ABNORMAL")
ABNORMAL
}
public enum FundsAccount {
@SerializedName("UNSETTLED")
UNSETTLED,
@SerializedName("AVAILABLE")
AVAILABLE,
@SerializedName("UNAVAILABLE")
UNAVAILABLE,
@SerializedName("OPERATION")
OPERATION,
@SerializedName("BASIC")
BASIC,
@SerializedName("ECNY_BASIC")
ECNY_BASIC
}
public static class Amount {
@SerializedName("total")
public Long total;
@SerializedName("refund")
public Long refund;
@SerializedName("from")
public List<FundsFromItem> from;
@SerializedName("payer_total")
public Long payerTotal;
@SerializedName("payer_refund")
public Long payerRefund;
@SerializedName("settlement_refund")
public Long settlementRefund;
@SerializedName("settlement_total")
public Long settlementTotal;
@SerializedName("discount_refund")
public Long discountRefund;
@SerializedName("currency")
public String currency;
@SerializedName("refund_fee")
public Long refundFee;
}
public static class Promotion {
@SerializedName("promotion_id")
public String promotionId;
@SerializedName("scope")
public PromotionScope scope;
@SerializedName("type")
public PromotionType type;
@SerializedName("amount")
public Long amount;
@SerializedName("refund_amount")
public Long refundAmount;
@SerializedName("goods_detail")
public List<GoodsDetail> goodsDetail;
}
public static class FundsFromItem {
@SerializedName("account")
public Account account;
@SerializedName("amount")
public Long amount;
}
public enum PromotionScope {
@SerializedName("GLOBAL")
GLOBAL,
@SerializedName("SINGLE")
SINGLE
}
public enum PromotionType {
@SerializedName("CASH")
CASH,
@SerializedName("NOCASH")
NOCASH
}
public static class GoodsDetail {
@SerializedName("merchant_goods_id")
public String merchantGoodsId;
@SerializedName("wechatpay_goods_id")
public String wechatpayGoodsId;
@SerializedName("goods_name")
public String goodsName;
@SerializedName("unit_price")
public Long unitPrice;
@SerializedName("refund_amount")
public Long refundAmount;
@SerializedName("refund_quantity")
public Long refundQuantity;
}
public enum Account {
@SerializedName("AVAILABLE")
AVAILABLE,
@SerializedName("UNAVAILABLE")
UNAVAILABLE
}
}

View File

@@ -0,0 +1,45 @@
# 支付成功回调通知
## 回调描述
用户使用普通支付APP支付/H5支付/JSAPI支付/Native支付/小程序支付功能当用户成功支付订单后微信支付会通过POST的请求方式向商户预先设置的回调地址APP支付/H5支付/JSAPI支付/Native支付/小程序支付下单接口传入的notify_url发送回调通知让商户知晓用户已完成支付。
> **注意**商户侧对微信支付回调IP有防火墙策略限制的需要对微信回调IP段开通白名单否则会导致收不到回调微信支付回调被商户防火墙拦截详情参考回调处理逻辑注意事项。
## 回调报文格式
微信支付会通过POST的方式向回调地址发送回调报文回调通知的请求主体中会包含JSON格式的通知参数
```json
{
"id": "EV-2018022511223320873",
"create_time": "2015-05-20T13:29:35+08:00",
"resource_type": "encrypt-resource",
"event_type": "TRANSACTION.SUCCESS",
"summary": "支付成功",
"resource": {
"original_type": "transaction",
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "",
"associated_data": "",
"nonce": ""
}
}
```
## 关键字段说明
| 字段 | 说明 |
|------|------|
| `event_type` | 事件类型,支付成功为 `TRANSACTION.SUCCESS` |
| `resource.algorithm` | 加密算法,固定为 `AEAD_AES_256_GCM` |
| `resource.ciphertext` | 密文需使用商户APIv3密钥解密后得到支付订单详情 |
| `resource.associated_data` | 附加数据,解密时作为 AAD 参数 |
| `resource.nonce` | 随机串,解密时作为 Nonce 参数 |
## 处理要求
1. 接收到回调后,先验证请求签名(使用微信支付公钥验签)
2. 使用商户APIv3密钥 + AEAD_AES_256_GCM 解密 `resource.ciphertext` 得到订单明文
3. 处理完成后返回 HTTP 200 + `{"code": "SUCCESS", "message": "成功"}` 表示确认收到
4. 若返回非200或超时微信支付会按策略重试通知

View File

@@ -0,0 +1,104 @@
# 基础支付 & 合单支付示例代码接口索引
> 根据用户确认的开发语言加载对应语言目录下的文件。Java 和 Go 目录结构一致。
## 下单(需确认支付方式)
| 支付方式 | 接口 | Java | Go |
|---------|------|------|-----|
| JSAPI/小程序 | POST /v3/pay/transactions/jsapi | `Java/1-JSAPI支付/JsapiPrepay.java` | `Go/1-JSAPI支付/JsapiPrepay.go` |
| APP | POST /v3/pay/transactions/app | `Java/2-APP支付/AppPrepay.java` | `Go/2-APP支付/AppPrepay.go` |
| H5 | POST /v3/pay/transactions/h5 | `Java/3-H5支付/H5Prepay.java` | `Go/3-H5支付/H5Prepay.go` |
| Native | POST /v3/pay/transactions/native | `Java/4-Native支付/NativePrepay.java` | `Go/4-Native支付/NativePrepay.go` |
## 调起支付(需确认支付方式,前端/客户端集成参考)
| 支付方式 | 调起方式 | Java |
|---------|---------|------|
| JSAPI | WeixinJSBridge | `Java/1-JSAPI支付/JsapiInvoke.md` |
| 小程序 | wx.requestPayment | `Java/5-小程序支付/MiniProgramInvoke.md` |
| APP | OpenSDK | `Java/2-APP支付/AppInvoke.md` |
| H5 | 跳转h5_url | `Java/3-H5支付/H5Invoke.md` |
| Native | code_url转二维码 | `Java/4-Native支付/NativeInvoke.md` |
## 通用接口(无需确认支付方式,各支付方式完全相同)
| 业务 | 接口 | Java | Go |
|------|------|------|-----|
| 微信订单号查单 | GET /v3/pay/transactions/id/{transaction_id} | `Java/6-订单查询/QueryByWxTradeNo.java` | `Go/6-订单查询/QueryByWxTradeNo.go` |
| 商户订单号查单 | GET /v3/pay/transactions/out-trade-no/{out_trade_no} | `Java/6-订单查询/QueryByOutTradeNo.java` | `Go/6-订单查询/QueryByOutTradeNo.go` |
| 关闭订单 | POST /v3/pay/transactions/out-trade-no/{out_trade_no}/close | `Java/7-关闭订单/CloseOrder.java` | `Go/7-关闭订单/CloseOrder.go` |
| 退款申请 | POST /v3/refund/domestic/refunds | `Java/8-订单退款/CreateRefund.java` | `Go/8-订单退款/CreateRefund.go` |
| 查询退款 | GET /v3/refund/domestic/refunds/{out_refund_no} | `Java/8-订单退款/QueryByOutRefundNo.java` | `Go/8-订单退款/QueryByOutRefundNo.go` |
| 异常退款 | POST /v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund | `Java/8-订单退款/CreateAbnormalRefund.java` | `Go/8-订单退款/CreateAbnormalRefund.go` |
| 支付回调通知 | 回调报文格式与处理要求 | `Java/9-支付回调通知/支付成功回调通知说明.md` | — |
| 退款回调通知 | 回调报文格式与处理要求 | `Java/10-退款结果回调通知/退款结果回调通知说明.md` | — |
| 申请交易账单 | GET /v3/bill/tradebill | `Java/11-申请交易账单/GetTradeBill.java` | `Go/11-申请交易账单/GetTradeBill.go` |
| 申请资金账单 | GET /v3/bill/fundflowbill | `Java/12-申请资金账单/GetFundFlowBill.java` | `Go/12-申请资金账单/GetFundFlowBill.go` |
| 下载账单 | GET download_url | `Java/13-下载账单/DownloadBill.java` | — |
---
## 合单支付下单(需确认支付方式)
> 合单支付不是独立的支付方式,而是将多个子订单合并为一笔支付的模式。下单 API 路径为 `/v3/combine-transactions/{type}`,与基础支付不同。
| 支付方式 | 接口 | Java | Go |
|---------|------|------|-----|
| JSAPI/小程序 | POST /v3/combine-transactions/jsapi | `Java/15-合单支付/UnionJsapiPrepay.java` | `Go/15-合单支付/UnionJsapiPrepay.go` |
| APP | POST /v3/combine-transactions/app | `Java/15-合单支付/UnionAppPrepay.java` | `Go/15-合单支付/UnionAppPrepay.go` |
| H5 | POST /v3/combine-transactions/h5 | `Java/15-合单支付/UnionH5Prepay.java` | `Go/15-合单支付/UnionH5Prepay.go` |
| Native | POST /v3/combine-transactions/native | `Java/15-合单支付/UnionNativePrepay.java` | `Go/15-合单支付/UnionNativePrepay.go` |
> 合单支付的**调起支付**与基础支付完全一致(返回的 `prepay_id` / `h5_url` / `code_url` 用法相同),直接复用上方「调起支付」中的文件即可。
## 合单支付专用接口(与基础支付不同,必须使用合单专用接口)
> 合单订单的查单和关单**不能**使用基础支付的查单/关单接口,必须使用以下合单专用接口。
| 业务 | 接口 | Java | Go |
|------|------|------|-----|
| 查询合单订单 | GET /v3/combine-transactions/out-trade-no/{combine_out_trade_no} | `Java/15-合单支付/UnionQueryByOutTradeNo.java` | `Go/15-合单支付/UnionQueryByOutTradeNo.go` |
| 关闭合单订单 | POST /v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close | `Java/15-合单支付/UnionClose.java` | `Go/15-合单支付/UnionClose.go` |
| 合单支付回调通知 | 回调报文格式与处理要求 | `Java/15-合单支付/合单支付成功回调通知说明.md` | — |
## 合单支付复用基础支付的接口
> 以下接口合单支付与基础支付完全相同,按**子单维度**操作。退款时使用子单的 `transaction_id`,账单以子单维度记录在各子单商户账单内。
| 业务 | 接口 | 参考文件 |
|------|------|---------|
| 退款申请 | POST /v3/refund/domestic/refunds | `Java/8-订单退款/CreateRefund.java``Go/8-订单退款/CreateRefund.go` |
| 查询退款 | GET /v3/refund/domestic/refunds/{out_refund_no} | `Java/8-订单退款/QueryByOutRefundNo.java``Go/8-订单退款/QueryByOutRefundNo.go` |
| 异常退款 | POST /v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund | `Java/8-订单退款/CreateAbnormalRefund.java``Go/8-订单退款/CreateAbnormalRefund.go` |
| 退款回调通知 | 回调报文格式与处理要求 | `Java/10-退款结果回调通知/退款结果回调通知说明.md` |
| 申请交易账单 | GET /v3/bill/tradebill | `Java/11-申请交易账单/GetTradeBill.java``Go/11-申请交易账单/GetTradeBill.go` |
| 申请资金账单 | GET /v3/bill/fundflowbill | `Java/12-申请资金账单/GetFundFlowBill.java``Go/12-申请资金账单/GetFundFlowBill.go` |
| 下载账单 | GET download_url | `Java/13-下载账单/DownloadBill.java` |
## 16. 分账
| 接口 | Java | Go |
|------|------|------|
| 请求分账 | `Java/16-分账/CreateOrder.java` | `Go/16-分账/CreateOrder.go` |
| 查询分账结果 | `Java/16-分账/QueryOrder.java` | `Go/16-分账/QueryOrder.go` |
| 请求分账回退 | `Java/16-分账/CreateReturnOrder.java` | `Go/16-分账/CreateReturnOrder.go` |
| 查询分账回退结果 | `Java/16-分账/QueryReturnOrder.java` | `Go/16-分账/QueryReturnOrder.go` |
| 解冻剩余资金 | `Java/16-分账/UnfreezeOrder.java` | `Go/16-分账/UnfreezeOrder.go` |
| 查询剩余待分金额 | `Java/16-分账/QueryOrderAmount.java` | `Go/16-分账/QueryOrderAmount.go` |
| 添加分账接收方 | `Java/16-分账/AddReceiver.java` | `Go/16-分账/AddReceiver.go` |
| 删除分账接收方 | `Java/16-分账/DeleteReceiver.java` | `Go/16-分账/DeleteReceiver.go` |
| 申请分账账单 | `Java/16-分账/SplitBill.java` | `Go/16-分账/SplitBill.go` |
> 分账规则、API 列表见 `3-商户与服务商通用/接入指南/分账接入指南.md`。
## SDK工具类所有接口的公共依赖
> 所有示例代码都依赖此工具类提供签名、验签、加解密、HTTP请求等基础能力。提醒用户需一并集成。
| 语言 | 文件 |
|------|------|
| Java | `Java/14-SDK工具类/WXPayUtility.java` — 签名、验签、加解密等基础能力 |
| Java | `Java/14-SDK工具类/WXPayClient.java` — HTTP 客户端,封装请求签名→发送→验签流程 |
| Go | `Go/14-SDK工具类/wxpay_utility.go` — 签名、验签、加解密等基础能力 |
| Go | `Go/14-SDK工具类/wxpay_client.go` — HTTP 客户端,封装请求签名→发送→验签流程 |

View File

@@ -0,0 +1,130 @@
# 商户开户意愿确认
商户开户意愿确认是微信支付的风控要求,商户入网后需完成开户意愿确认,确保商户主体名称不被冒用。渠道商(银行、支付机构、普通服务商)可通过两种方式协助商户完成确认。
## 两种确认方式
| 方式 | 说明 |
|------|------|
| "商家注册"小程序 | 渠道商下载拓展二维码,商家扫码在小程序中提交资料并完成确认 |
| 商户开户意愿确认 API | 渠道商通过 API 提交商家资料,联系人扫小程序码确认信息并完成验证 |
> API 方式仅减少商家在小程序提交资料的环节,联系人仍需扫描小程序码进行确认。
## API 流程
```
1. 渠道商获取商户号(通过进件或已有商户号)
2. 渠道商创建开户意愿申请单
├── 调用「提交申请单」接口,提交商家资料 + 商户号
└── 在补充材料 - 待确认商户号列表中传入商户号,联系人确认时可同步完成授权
3. 微信支付审核1-3 个工作日)
├── 审核通过 → 状态变为"待联系人确认"
└── 审核驳回 → 根据驳回原因修改资料,调用「撤销申请单」后重新提交
4. 联系人确认(扫描申请单小程序码)
├── 核对联系信息(可修改手机号),完成短信验证
└── 核实商家资质信息,点击确认
5. 商家授权商户号
├── 若创建申请单时已传入商户号列表 → 联系人在确认流程中直接授权
└── 若未传入 → 联系人后续通过小程序码或公众号消息进入授权页面
⚠️ 只有商户全称与注册商家名称一致的商户号才能授权
6. 账户验证(按主体类型和联系人身份判断)
├── 个体工商户(法人/经营者)→ 无需验证
├── 个体工商户(负责人)→ 法人扫码验证
├── 企业(法人/经营者)→ 无需验证
├── 企业(负责人)→ 汇款验证或法人扫码10天有效
├── 党政/事业单位 → 无需验证
├── 其他组织 → 汇款验证10天有效
└── 小微 → 法人/经营者验证
7. 渠道商确认完成
├── 查询申请单状态为"审核通过"APPLYMENT_STATE_PASSED
└── 查询授权状态为"已授权"
```
## API 接口
| 接口 | 方法 | 路径 | 备注 |
|------|------|------|------|
| 提交申请单 | POST | `/v3/apply4subject/applyment/` | 频率限制5QPS商户号维度 |
| 查询申请单审核结果 | GET | `/v3/apply4subject/applyment` | 通过 applyment_id 或 business_code 查询,频率 20QPS |
| 撤销申请单 | POST | `/v3/apply4subject/applyment/{business_code}/cancel``{applyment_id}/cancel` | 驳回后修改前须先撤销 |
| 获取商户开户意愿确认状态 | GET | `/v3/apply4subject/applyment/merchants/{sub_mchid}/state` | 确认商户是否已完成授权 |
> 敏感信息加密:联系人姓名、手机号、身份证号等敏感字段须使用微信支付公钥(推荐)或平台证书公钥加密,请求 Header 须携带 `Wechatpay-Serial`。
## 申请单状态流转
| 状态 | 含义 | 后续操作 |
|------|------|---------|
| APPLYMENT_STATE_EDITTING | 编辑中 | 提交可能发生错误,可用同一 business_code 重新提交 |
| APPLYMENT_STATE_WAITTING_FOR_AUDIT | 审核中 | 等待 1-2 个工作日 |
| APPLYMENT_STATE_WAITTING_FOR_CONFIRM_CONTACT | 待联系人确认 | 获取小程序码,引导联系人扫码确认联系信息 |
| APPLYMENT_STATE_WAITTING_FOR_CONFIRM_LEGALPERSON | 待账户验证 | 引导联系人扫码完成账户验证 |
| APPLYMENT_STATE_PASSED | 审核通过(终态) | 引导联系人扫码完成授权商户号 |
| APPLYMENT_STATE_REJECTED | 审核驳回(终态) | 根据驳回原因修改,调用撤销接口后重新提交 |
| APPLYMENT_STATE_FREEZED | 已冻结(终态) | 主体已完成过入驻,查看驳回原因,通知指定联系人扫码完成授权 |
| APPLYMENT_STATE_CANCELED | 已作废(终态) | 申请单已被撤销或超 720 小时未完成 |
## 与特约商户进件的关系
| 维度 | 特约商户进件 | 商户开户意愿确认 |
|------|-----------|----------------|
| 目的 | 商户入驻微信支付,获取商户号 | 确认商户开户意愿,防止主体被冒用 |
| 使用方 | 普通服务商 | 渠道商(银行、支付机构、普通服务商) |
| API 前缀 | `/v3/applyment4sub/` | `/v3/apply4subject/` |
| 是否必须 | 服务商拓展子商户时必须 | 根据风控要求,部分商户入网后需完成 |
| 流程 | 服务商提交全部资料 → 审核 → 签约 | 渠道商提交基础资料 → 审核 → 联系人扫码确认 |
## 重要参数说明
| 参数 | 说明 |
|------|------|
| `business_code` | 业务申请编号,服务商自定义唯一编号,每个编号对应一个申请单 |
| `subject_type` | 主体类型枚举:`SUBJECT_TYPE_ENTERPRISE`(企业)、`SUBJECT_TYPE_INDIVIDUAL`(个体工商户)、`SUBJECT_TYPE_INSTITUTIONS_CLONED`(事业单位)、`SUBJECT_TYPE_OTHERS`(其他组织)、`SUBJECT_TYPE_MICRO`(小微商户) |
| `licence_number` | 营业执照注册号,个体工商户或企业须为 15 位数字或 18 位数字/大写字母 |
| `merchant_name` | 商户名称须与营业执照一致;个体工商户不能以"公司"结尾;营业执照名称为空时填"个体户+经营者姓名" |
| `confirm_mchid_list` | 补充材料中的待确认商户号列表,传入后联系人确认时可同步完成授权,无需二次授权 |
| 敏感字段 | 联系人姓名、手机号、身份证号须加密Header 须携带 `Wechatpay-Serial` |
## 常见问题
### 业务规则
| 问题 | 答案 |
|------|------|
| 一个微信号可同时提交多个申请单吗? | 不可以,一个微信只能有一个流程中的申请单 |
| 申请单有效期多长? | 720 小时30 天),超期自动作废 |
| 账户验证需要多长时间? | 汇款后 2 小时内获知结果;法人扫码后立即完成 |
| 审核驳回后怎么重新提交? | 须先调用「撤销申请单」接口,再用新的 business_code 重新提交 |
| 商户名称有什么要求? | 必须与营业执照上的商户名称一致 |
| "审核通过"是最终状态吗? | 是,"审核通过""已冻结""已作废"均为终态 |
| 联系人微信号有什么要求? | 联系人微信实名的身份证号需与申请单填写的联系人身份证号一致 |
| 什么是"已冻结"状态? | 该主体已完成过入驻,需查看驳回原因,通知指定联系人扫码完成授权 |
| 进行开户意愿确认前是否需要先绑定 sub_mchid 与 sub_appid 的关系? | 不需要,需要先完成开户意愿确认 |
| 查询申请单审核结果接口返回的 qrcode_data小程序码图片有效期是多久 | 有效期 30 天,超过 30 天后需要先撤销申请单,再重新提交 |
| 申请单状态为 APPLYMENT_STATE_PASSED 但返回的二维码已过期怎么办? | 二维码有效期 30 天,过期后可撤销申请单再重新提交,也可调用「获取商户开户意愿确认状态」接口查询商户是否已完成授权 |
| 授权完成后,申请单返回的二维码还有使用场景吗? | 已完成授权后,二维码一般不再有使用场景 |
| 商户名称中的"特殊符号"怎么定义? | @#$%° 等均算特殊字符;商户名称不能使用纯数字或纯特殊字符,但名称中可以包含数字和特殊字符,按照营业执照上的正确名称填写即可 |
| 商户名称字段规则中"不能为纯数字"和"可以包含数字"是否冲突? | 不冲突,规则含义是商户名称不能是纯数字或纯特殊字符,但名称中允许包含数字和特殊字符 |
| 获取商户开户意愿确认状态接口有限频吗? | 有,限频为 10QPS |
### 常见报错
| 报错信息 | 原因与解决 |
|---------|-----------|
| "暂未查询到该营业执照注册号" | `licence_number` 填写错误,个体工商户/企业须为 15 位数字或 18 位数字/大写字母 |
| "系统繁忙,请稍后重试" | ① 系统异常稍后重试;② 参数大小写/格式与文档不一致;③ 请求头 `mchid` 填写错误 |
| "查询申请单不存在" | `applyment_id` 填写错误或未填写,申请单编号和业务申请编号至少传一个 |
| "Authorization不合法" | ① 认证类型须为 `WECHATPAY2-SHA256-RSA2048`;② `mchid` 须为渠道商商户号;③ 检查证书序列号、签名值;④ Authorization 头不能有换行 |
| "商户未申请过证书" | 须到商户平台申请下载 API 证书并正确使用 |
| "无法将传入参数'申请单编号'转换为uint64类型" | `applyment_id` 参数类型错误,须为 uint64 |
| "注册号/证书号填写错误或与证书类型不匹配" | 检查 `licence_number``cert_number` 是否与所选的证书类型(`cert_type`)匹配,确认格式和内容正确 |
| "生成小程序二维码失败,请稍后再试[40001]" | 已知偶发问题,直接重试即可 |
| "请填写有效身份证居住地址" | `identification_address` 字段填写有误,需填写身份证上的有效居住地址 |
| "请选择门店所在省市区范围" | `store_address_code` 字段填写有误,需选择正确的门店所在省市区行政区划代码 |

View File

@@ -0,0 +1,80 @@
# 开发必要参数说明(服务商模式)
> 两种接入模式的参数、API路径等核心差异见 `接入模式说明.md`,本文仅补充服务商模式的落地细节。
> 未配置 `sub_appid` 时只能传 `openid`(即 sp_appid 下的用户标识)。
## 权限申请(各支付方式流程一致)
1. **服务商申请开通**:登录服务商平台 → 产品中心 → 特约商户授权产品 → 对应支付产品 → 申请开通(审核 7 个工作日)
2. **子商户授权**:服务商在特约商户列表中发起邀请 → 子商户登录商户平台 → 产品中心 → 我的授权产品 → 授权
> 子商户需先通过服务商平台或接口入驻。如无服务商商户号,需先申请入驻。
## APPID 绑定
- **sp_appid**:服务商平台 → 产品中心 → APPID授权管理 → 关联 APPID
- **sub_appid**(可选,由服务商操作):服务商平台 → 合作伙伴功能 → 开发参数配置 → 对应子商户 → 特约商户APPID配置
APPID 类型(公众号/小程序/移动应用)与商户模式相同,**三种格式相同但不能混用**。
| 常见报错 | 原因 | 处理 |
|---------|------|------|
| `appid and mchid not match` | sp_appid/sub_appid 与对应商户号未绑定 | 检查服务商平台 APPID 绑定配置 |
| `appid is invalid` | appid 格式错或类型不对 | 确认使用了正确类型的 APPID |
## 各支付方式速查表
> 通用下单参数(各方式相同):`sub_mchid`(收款子商户号)、`time_expire`(默认 7 天)、`profit_sharing`(分账标识)。
> 通用后续流程:查询订单、支付回调通知、下载账单、退款,各支付方式完全一致。
| | JSAPI | APP | H5 | Native | 小程序 |
|--|-------|-----|----|---------|----|
| **适用场景** | 微信内浏览器网页 | 原生APP | 手机浏览器(非微信) | PC端网页 | 微信小程序 |
| **下单API** | `.../jsapi` | `.../app` | `.../h5` | `.../native` | 同JSAPI |
| **下单产物** | prepay_id2h | prepay_id2h | h5_url**5min** | code_url2h | prepay_id2h |
| **调起方式** | WeixinJSBridge | OpenSDK sendReq | 跳转h5_url | code_url转二维码 | wx.requestPayment |
| **需openid** | ✅ | ❌ | ❌ | ❌ | ✅ |
> 下单 API 路径前缀均为 `/v3/pay/partner/transactions`。小程序与 JSAPI **共享权限和下单接口**。
> 付款码支付仅有 V2 接口XML + MD5/HMAC-SHA256不在上表 V3 体系中参考文档https://pay.weixin.qq.com/doc/v2/partner/4011941052
## 各支付方式特殊注意
### JSAPI
- **必须接入点金计划**:服务商模式下 JSAPI 支付完成后,页面会被替换为点金计划官方页面,服务商的 H5/小程序页面会被关闭。不接入商家小票功能则支付后用户无法返回商户页面。详见 `点金计划.md`
- 必须在服务商平台配置授权目录
### APP
- 调起支付的 `appId` 必须与下单参数一致服务商APP中传 `sp_appid`子商户APP中传 `sub_appid``sub_appid` 仅在子商户APP下单时需传
- Android 必须实现 `WXPayEntryActivity`Android 13 需去除其 `intent-filter`
- iOS 务必配置 URL scheme
- 严格遵循 OpenSDK 接入指引(安卓/iOS/鸿蒙)
### H5
- **不能在APP内使用**
- 必须在服务商平台配置 H5 支付域名
- 可在 `h5_url` 后拼接 `redirect_url` 指定支付后返回页面
- **必须实现跨域安全校验**OPTIONS 预检和 GET/POST 请求均需校验 `Origin` 白名单和用户登录态,否则可能被依据《微信支付服务协议》处理
### Native
- 仅支持**扫一扫**,不支持相册识别或长按识别二维码
- 支付完成后**无前端回调**(用户留在微信端),必须依赖回调通知或轮询查单确认状态
### 小程序
- 与 JSAPI 区别仅在调起方式wx.requestPayment和 openid 获取方式wx.login无需配置授权目录
- 小程序内嵌 H5 页面**不能**调用 JSAPI 收款,只能用小程序支付
- 调起支付签名的 `appid` 需与实际调起的小程序一致(服务商小程序用 `sp_appid`,子商户小程序用 `sub_appid`
- 交易类小程序须满足《交易类小程序运营规范》,结算周期受公众平台管控
## 参数与代码示例的对应关系
```
spMchid → 服务商商户号
subMchid → 子商户号
spAppid → 服务商APPID
subAppid → 子商户APPID可选
certificateSerialNo → 服务商API证书序列号
privateKeyFilePath → 服务商API证书私钥文件路径apiclient_key.pem
wechatPayPublicKeyId → 微信支付公钥ID
wechatPayPublicKeyFilePath → 微信支付公钥文件路径wxp_pub.pem
```

View File

@@ -0,0 +1,108 @@
# 点金计划(服务商 JSAPI 支付必接)
## 什么是点金计划
点金计划是微信支付官方提供的支付后页面升级能力。服务商为子商户开通后,子商户的 **JSAPI 支付完成页**将被替换为点金计划官方页面,同时发起支付的服务商 H5 页面或小程序页面**会被关闭**
**核心影响**:如果服务商不接入商家小票功能,支付后用户将看不到商户的业务内容(取餐码、停车时长、订单详情等),也无法返回商户页面。
## 两种小票模式
| | 官方小票 | 商家小票 |
|--|---------|---------|
| **内容** | 平台自动生成,仅展示订单金额等基础信息 | 商家自定义,可展示取餐码、停车时长等业务内容 |
| **开发成本** | 无需开发,直接开通 | 需开发商家小票页面并完成 iframe 对接 |
| **适用场景** | 无需展示业务信息的子商户 | 需要在支付后展示业务信息的子商户(餐饮、停车等) |
## 开通流程
### 1. 服务商开通点金计划
路径:服务商平台 → 服务商功能 → 点金计划 → 申请开通(签署承诺函 + 填写业务联络人)。
### 2. 为子商户开通官方小票
路径:服务商平台 → 服务商功能 → 点金计划 → 特约商户管理 → 打开"点金计划"开关。
也可通过"默认开通"按钮为全量子商户自动开通(有 JSAPI 支付的子商户会自动开通,无 JSAPI 支付权限的不会)。
> 主动关闭点金计划后,需间隔 **24 小时**方可再次开通。
### 3. 为子商户开通商家小票(可选)
步骤:
1. 配置商家小票链接:服务商平台 → 服务商功能 → 点金计划 → 商家小票链接配置 → 添加链接(须 HTTPS + ICP 备案)→ 下载验证文件放到链接目录下验证所有权
2. 打开商家小票开关:特约商户管理 → 打开"商家小票"开关
3. 打开点金计划开关:特约商户管理 → 打开"点金计划"开关
> 先开商家小票再开点金计划,避免时间差导致部分订单展示官方小票。
> 部分子商户(医院、学校等)可配置独立的商家小票链接。
## 商家小票开发要点
点金计划页面上方提供一个 **iframe 框架**,商家小票页面嵌入其中。核心交互流程:
1. 用户 JSAPI 支付完成 → 点金计划页面加载 → iframe 加载商家小票链接
2. 点金计划页面向 iframe 传递订单信息(通过 postMessage
3. 商家小票页面获取订单信息、校验通过后展示业务内容
4. **必须在 3 秒内调用父页面的 `onIframeReady` JSAPI**,否则提示"无法获取订单信息"
## 小程序左上角返回键管理
小程序支付场景下,可通过 `wx.requestPayment``payCompletedPageOptions` 控制点金计划页面左上角的返回按钮。
**隐藏返回按钮**
```javascript
wx.requestPayment({
payCompletedPageOptions: {
showNavBackButton: false
}
})
```
**自定义返回按钮跳转页面**
```javascript
wx.requestPayment({
payCompletedPageOptions: {
showNavBackButton: true
},
success: res => {
res.payCompletedPage.onUnload(() => {
wx.navigateTo({
url: '/page/index/index'
})
})
}
})
```
> 点击左上角返回按钮会触发支付回调页的 `onUnload` 事件,在该事件中可跳转到指定页面。
## API 接口
| 接口 | 方法 | 路径 | 用途 |
|------|------|------|------|
| 点金计划管理 | POST | `/v3/goldplan/merchants/changegoldplanstatus` | 为子商户开通/关闭点金计划 |
| 商家小票管理 | POST | `/v3/goldplan/merchants/changecustompagestatus` | 为子商户开通/关闭商家小票 |
| 同业过滤标签 | POST | `/v3/goldplan/merchants/set-advertising-industry-filter` | 过滤同行业广告(最多 3 个标签) |
| 开通广告展示 | PATCH | `/v3/goldplan/merchants/open-advertising-show` | 开通广告并设置过滤标签 |
| 关闭广告展示 | POST | `/v3/goldplan/merchants/close-advertising-show` | 关闭广告展示 |
> 所有接口均需传 `sub_mchid`(子商户号)和 `operation_type`OPEN/CLOSE
## 常见问题
| 问题 | 答案 |
|------|------|
| 开通/关闭点金计划后多久生效? | 开通后 **5 分钟**内生效 |
| 看不到"商家小票"开关? | 需先在"商家小票链接配置"中添加链接,开关才会出现 |
| 商家小票调试报"无法获取订单信息" | ① 确认已打开商家小票和点金计划开关;② 页面须在 3 秒内调用 `onIframeReady` JSAPI③ 扫码调试的微信号须与支付订单的微信号一致;④ 商家小票页面须可正常访问 |
| 开通后支付完成页展示什么? | 只展示点金计划页面(含官方/商家小票 + 广告),不再展示服务商的支付回调页 |
| 服务商返佣怎么算? | 返佣(元)= eCPM/ 1000 × 支付后广告曝光量 |
| 如何领取广告收益的技术服务费? | 参考技术服务费结果查询、技术服务费领取指引查询和领取 |
| 自定义页面点按钮弹框"即将打开商家的新页面"能去掉吗? | **不支持**去掉或自定义弹窗内容 |
| jumpOut 跳转公众号时弹窗显示商家简称,能隐藏或修改吗? | **不支持** |
| 商家小票页面支持跳转小程序吗? | 小程序支付场景支持用点金计划 JS 跳转小程序JSAPI 支付场景**不支持** |
| 从业机构号可以开通点金计划吗? | **不支持**,需要从业机构下的渠道商去开通配置 |

View File

@@ -0,0 +1,146 @@
# 特约商户进件
特约商户进件是微信支付面向**普通服务商**开放的接口能力,用于协助各类型商户发起入驻微信支付的申请。从业机构(银行及支付机构)及电商平台不可使用本接口。
## 支持的商户类型
| 类型 | 定义 | 所需资料 |
|------|------|---------|
| 个体工商户 | 营业执照主体类型为个体户/个体工商户/个体经营 | 营业执照、经营者证件、结算银行账户 |
| 企业 | 营业执照主体类型为有限公司/有限责任公司 | 营业执照、法人证件、组织机构代码证(未三证合一)、结算银行账户 |
| 党政/机关及事业单位 | 各级政府机构、事业单位 | 登记证书、法人证件、组织机构代码证(未三证合一)、结算银行账户 |
| 其他组织 | 社会团体、民办非企业、基金会等 | 登记证书、法人证件、组织机构代码证(未三证合一) |
> 不支持进件个人小微商户。
## 进件流程
```
1. 服务商提交进件申请
├── 调用「提交申请单」接口,填写:主体信息、经营信息、结算规则、结算账户、超级管理员
└── 调用「查询申请单状态」获取签约链接(建议转二维码),发给超级管理员
└── 超管扫码后,"微信支付商家助手"公众号推送申请进展通知
2. 微信支付审核(约 1-3 个工作日)
3. 特约商户账户验证(审核通过后,按主体类型判断是否需要)
├── 个体工商户:超管为法人/经营者 → 无需验证;超管为负责人 → 法人扫码验证10天有效
├── 企业:超管为法人/经营者 → 无需验证;超管为负责人 → 汇款验证或法人扫码验证10天有效
├── 党政/事业单位:上传盖章证明函 → 无需验证;汇款验证 → 指定账户汇款10天有效
└── 其他组织:汇款账户验证
⚠️ 10天内未完成验证申请单自动驳回
4. 特约商户签约
├── 服务商通过查询接口获取签约链接,发给超管
└── 超管扫码完成签约
5. 开通权限(签约后约 30 分钟自动完成)
├── 根据进件时选择的经营场景授权支付权限:
│ ├── 线下门店 → 付款码、JSAPI、刷脸
│ ├── 公众号 → JSAPI
│ ├── 小程序 → JSAPI
│ ├── App → App支付
│ └── 互联网网站 → JSAPI、Native
└── 默认授权 API 退款权限
6. 完成入驻(申请单状态为"已完成"
└── 首笔交易后,微信支付汇款 0.01 元至结算银行卡验证账户信息
```
## API 接口
| 接口 | 方法 | 路径 | 备注 |
|------|------|------|------|
| 提交申请单 | POST | `/v3/applyment4sub/applyment/` | 频率限制15QPS服务商维度 |
| 通过业务申请编号查询申请状态 | GET | `/v3/applyment4sub/applyment/business_code/{business_code}` | |
| 通过申请单ID查询申请状态 | GET | `/v3/applyment4sub/applyment/applyment_id/{applyment_id}` | |
| 修改结算账户 | POST | `/v3/apply4sub/sub_merchants/{sub_mchid}/modify-settlement` | 每天最多 5 次,频率限制 20/min |
| 查询结算账户 | GET | `/v3/apply4sub/sub_merchants/{sub_mchid}/settlement` | |
| 查询结算账户修改申请状态 | GET | `/v3/apply4sub/sub_merchants/{sub_mchid}/application/{application_no}` | |
| 图片上传 | POST | `/v3/merchant/media/upload` | 进件资料中的图片需先上传获取 MediaID |
> 敏感信息加密:进件请求中的姓名、身份证号、手机号、邮箱等敏感字段需使用**微信支付公钥**(推荐)或平台证书公钥加密,请求 Header 须携带 `Wechatpay-Serial`公钥ID 或证书序列号)。
## 提交申请单核心参数结构
| 模块 | 必填 | 主要字段 |
|------|------|---------|
| `business_code` | 是 | 业务申请编号,服务商自定义唯一编号。若申请单被驳回,可填相同编号覆盖修改原申请单 |
| `contact_info` | 是 | 超级管理员信息类型LEGAL 法人/SUPER 经办人)、姓名🔐、手机🔐、邮箱🔐 |
| `subject_info` | 是 | 主体资料:主体类型、营业执照/登记证书、法人身份证件(姓名🔐、证件号🔐) |
| `business_info` | 是 | 经营资料:商户简称、经营场景(线下/公众号/小程序/App/网站、AppID |
| `settlement_info` | 是 | 结算规则:结算账户类型(对公/对私)、开户银行、银行账号 |
| `bank_account_info` | 是 | 结算银行账户:开户名称🔐、银行账号🔐、开户银行 |
| `addition_info` | 否 | 补充材料:业务流程截图、业务合同等 |
> 🔐 标记的字段为敏感信息需使用微信支付公钥或平台证书公钥加密HTTP 头须上送 `Wechatpay-Serial`。
>
> 超级管理员类型为 `SUPER`(经办人)时,需额外上传经办人身份证件和业务办理授权函。
### 重要参数说明
| 参数 | 说明 |
|------|------|
| `business_code` | 服务商自定义唯一编号,建议前缀为服务商商户号。每个编号对应一个申请单,审核通过后生成微信支付商户号。被驳回可用相同编号覆盖修改 |
| `bank_account_type` | 账户类型。企业/党政/事业/其他组织:`BANK_ACCOUNT_TYPE_CORPORATE`(对公)。个体户可选对公或 `BANK_ACCOUNT_TYPE_PERSONAL`(经营者个人银行卡) |
| `account_name` | 开户名称。选"经营者个人银行卡"时须与经营者证件姓名一致;选"对公银行账户"时须与营业执照/登记证书的商户名称一致 |
| `subject_type` | 主体类型枚举:`SUBJECT_TYPE_INDIVIDUAL`(个体户)、`SUBJECT_TYPE_ENTERPRISE`(企业)、`SUBJECT_TYPE_GOVERNMENT`(政府机关)、`SUBJECT_TYPE_INSTITUTIONS`(事业单位)、`SUBJECT_TYPE_OTHERS`(社会组织) |
| 不需要的参数 | **不要传空值**,不需要的字段直接不上传,否则可能报"系统繁忙" |
## 申请单状态流转
| 状态 | 含义 | 后续操作 |
|------|------|---------|
| APPLYMENT_STATE_EDITTING | 编辑中 | — |
| APPLYMENT_STATE_AUDITING | 审核中 | 等待审核结果1-3个工作日 |
| APPLYMENT_STATE_REJECTED | 已驳回 | 根据驳回原因修改后重新提交 |
| APPLYMENT_STATE_TO_BE_CONFIRMED | 待账户验证 | 发送验证链接给超管 |
| APPLYMENT_STATE_TO_BE_SIGNED | 待签约 | 发送签约链接给超管 |
| APPLYMENT_STATE_SIGNING | 开通权限中 | 约30分钟自动完成 |
| APPLYMENT_STATE_FINISHED | 已完成 | 入驻成功 |
| APPLYMENT_STATE_CANCELED | 已作废 | 超30天未签约自动作废 |
## 常见问题
### 业务规则
| 问题 | 答案 |
|------|------|
| 服务商可以入驻成为自己的特约商户吗? | 不可以 |
| 普通商户号可以和服务商号建立绑定关系吗? | 不支持,需重新在该服务商下进件 |
| 特约商户能否变更为普通服务商? | 不支持 |
| 申请单已审核通过但想修改信息? | 没有撤销接口可更换进件单号重新提交。原申请单30天后自动作废 |
| 重新提交进件返回的申请单号不变? | 需更换 `business_code`(业务申请编号),否则复用旧申请单 |
| 提交申请单接口频率限制? | 服务商维度 15QPS |
| 修改结算银行卡频率限制? | 每天最多提交 5 次,次日 0 点后可重新发起 |
| 法人与营业执照上不一致? | 需先完成工商信息变更,确保一致后再进件 |
| 查询结算账户不返回 `verify_result` 字段? | 入驻后若没修改过银行卡,除非汇款失败,否则不返回该字段 |
### 常见报错
| 报错信息 | 原因与解决 |
|---------|-----------|
| "暂无权限" / "NO_AUTH" | ① 服务商商户号被处罚,进入合作伙伴平台 → 合作伙伴功能 → 风险处理 → 功能限制记录,找到对应单据处理;② 请求头 `mchid` 填写错误,只能填普通服务商商户号;③ 不允许进件小微商户 |
| "进件特约商户的权限已被受限" | 服务商进件权限被风控限制,登录服务商平台查看受限原因,解除限制后再调用接口 |
| "身份证号码,与营业执照不匹配" | `id_card_number` 需填写营业执照上法人/经营者的身份证号,格式为 17位数字+1位数字/X且须加密 |
| "请填写法人证件姓名" | `id_card_name` 字段未传或传值有误,确认填写后重新提交 |
| "经营者/负责人证件类型取值不在有效范围内" | 未传 `contact_type` 字段,需传入 `LEGAL``SUPER` |
| "参数'组织机构代码证照片'是必填项" | 不需要的参数结构不要传入,去掉 `organization_info` 结构 |
| "系统繁忙,请稍后重试" | ① 系统繁忙稍后重试;② `mchid` 填写错误;③ 不需要的参数不要上传,不能传空值 |
| "开户银行取值有误" | 开户银行全称和联行号须匹配(如联行号 310290097606 对应"上海浦东发展银行股份有限公司三林支行"),调用「查询支持个人/对公业务的银行列表」接口获取正确值 |
| "证书图片内容无效" | 调用图片上传接口重新获取 MediaID |
| 命中敏感词INVALID_REQUEST | 商户名称或简称含敏感词(如"慈善""基金""医疗"等),修改后重新提交。具体敏感词不对外公开,需逐步排查 |
| "商户号数量超过上限" | 系统基于已有商户号经营情况动态评估上限,注销闲置商户号后需等系统重新评估,评估标准为黑盒 |
### 线上高频问题(来自真实工单)
| 问题 | 答案 |
|------|------|
| 电商服务商调进件接口报错? | 电商服务商无权限调用特约商户进件接口,需先接入为**普通服务商**,参考特约商户进件文档 |
| 进件时 AppID 填什么? | 填写与商家主体一致且已认证的应用 AppID需是已认证的 App在"经营信息"模块按指引填写 |
| 申请单一直"资料校验中"不变? | 可能单据已驳回或已作废,调用查询申请单状态接口确认实际状态及驳回原因 |
| 状态为 ACCOUNT_NEED_VERIFY 但未返回 `legal_validation_url` | 接口根据商户当前所需操作返回对应链接,当天商户应先操作账户验证 |
| 申请单卡在"待确认"无法取消或删除? | 没有取消/删除接口,审核需 1-3 个工作日30 天不处理自动作废。可更换 `business_code` 重新提交 |
| API 进件的商户如何修改商户简称? | 通过商户平台修改,不支持通过 API 修改 |
| 单个人/主体进件次数有限制吗? | 有限制,系统动态评估,具体以实际返回为准 |
| 商户已存在怎么办? | 根据提示先注销已有商户号,再重新进件 |

View File

@@ -0,0 +1,211 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/partner/4013080340
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &PartnerApiv3PartnerJsapiPrepayRequest{
SpAppid: wxpay_utility.String("wx8888888888888888"),
SpMchid: wxpay_utility.String("1230000109"),
SubAppid: wxpay_utility.String("wxd678efh567hg6999"),
SubMchid: wxpay_utility.String("1900000109"),
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
TimeExpire: wxpay_utility.Time(time.Now()),
Attach: wxpay_utility.String("自定义数据"),
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
GoodsTag: wxpay_utility.String("WXG"),
SettleInfo: &PartnerSettleInfo{
ProfitSharing: wxpay_utility.Bool(true),
},
SupportFapiao: wxpay_utility.Bool(true),
Amount: &CommonAmountInfo{
Total: wxpay_utility.Int64(100),
Currency: wxpay_utility.String("CNY"),
},
Payer: &PartnerJsapiReqPayerInfo{
SpOpenid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
SubOpenid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
},
Detail: &CouponInfo{
CostPrice: wxpay_utility.Int64(608800),
InvoiceId: wxpay_utility.String("微信123"),
GoodsDetail: []GoodsDetail{
GoodsDetail{
MerchantGoodsId: wxpay_utility.String("1246464644"),
WechatpayGoodsId: wxpay_utility.String("1001"),
GoodsName: wxpay_utility.String("iPhoneX 256G"),
Quantity: wxpay_utility.Int64(1),
UnitPrice: wxpay_utility.Int64(528800),
},
},
},
SceneInfo: &CommonSceneInfo{
PayerClientIp: wxpay_utility.String("14.23.150.211"),
DeviceId: wxpay_utility.String("013467007045764"),
StoreInfo: &StoreInfo{
Id: wxpay_utility.String("0001"),
Name: wxpay_utility.String("腾讯大厦分店"),
AreaCode: wxpay_utility.String("440305"),
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
},
},
}
response, err := PartnerJsapiPrepay(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func PartnerJsapiPrepay(config *wxpay_utility.MchConfig, request *PartnerApiv3PartnerJsapiPrepayRequest) (response *PartnerApiv3PartnerJsapiPrepayResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/pay/partner/transactions/jsapi"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &PartnerApiv3PartnerJsapiPrepayResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type PartnerApiv3PartnerJsapiPrepayRequest struct {
SpAppid *string `json:"sp_appid,omitempty"`
SpMchid *string `json:"sp_mchid,omitempty"`
SubAppid *string `json:"sub_appid,omitempty"`
SubMchid *string `json:"sub_mchid,omitempty"`
Description *string `json:"description,omitempty"`
OutTradeNo *string `json:"out_trade_no,omitempty"`
TimeExpire *time.Time `json:"time_expire,omitempty"`
Attach *string `json:"attach,omitempty"`
NotifyUrl *string `json:"notify_url,omitempty"`
GoodsTag *string `json:"goods_tag,omitempty"`
SettleInfo *PartnerSettleInfo `json:"settle_info,omitempty"`
SupportFapiao *bool `json:"support_fapiao,omitempty"`
Amount *CommonAmountInfo `json:"amount,omitempty"`
Payer *PartnerJsapiReqPayerInfo `json:"payer,omitempty"`
Detail *CouponInfo `json:"detail,omitempty"`
SceneInfo *CommonSceneInfo `json:"scene_info,omitempty"`
}
type PartnerApiv3PartnerJsapiPrepayResponse struct {
PrepayId *string `json:"prepay_id,omitempty"`
}
type PartnerSettleInfo struct {
ProfitSharing *bool `json:"profit_sharing,omitempty"`
}
type CommonAmountInfo struct {
Total *int64 `json:"total,omitempty"`
Currency *string `json:"currency,omitempty"`
}
type PartnerJsapiReqPayerInfo struct {
SpOpenid *string `json:"sp_openid,omitempty"`
SubOpenid *string `json:"sub_openid,omitempty"`
}
type CouponInfo struct {
CostPrice *int64 `json:"cost_price,omitempty"`
InvoiceId *string `json:"invoice_id,omitempty"`
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
}
type CommonSceneInfo struct {
PayerClientIp *string `json:"payer_client_ip,omitempty"`
DeviceId *string `json:"device_id,omitempty"`
StoreInfo *StoreInfo `json:"store_info,omitempty"`
}
type GoodsDetail struct {
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
GoodsName *string `json:"goods_name,omitempty"`
Quantity *int64 `json:"quantity,omitempty"`
UnitPrice *int64 `json:"unit_price,omitempty"`
}
type StoreInfo struct {
Id *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
AreaCode *string `json:"area_code,omitempty"`
Address *string `json:"address,omitempty"`
}

View File

@@ -0,0 +1,168 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/partner/4013080340
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &GetTradeBillRequest{
BillDate: wxpay_utility.String("2019-06-11"),
SubMchid: wxpay_utility.String("19000000001"),
BillType: BILLTYPE_ALL.Ptr(),
TarType: TARTYPE_GZIP.Ptr(),
}
response, err := GetTradeBill(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func GetTradeBill(config *wxpay_utility.MchConfig, request *GetTradeBillRequest) (response *QueryBillEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/bill/tradebill"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
query := reqUrl.Query()
query.Add("bill_date", *request.BillDate)
query.Add("sub_mchid", *request.SubMchid)
query.Add("bill_type", fmt.Sprintf("%v", *request.BillType))
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &QueryBillEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type GetTradeBillRequest struct {
BillDate *string `json:"bill_date,omitempty"`
SubMchid *string `json:"sub_mchid,omitempty"`
BillType *BillType `json:"bill_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
}
func (o *GetTradeBillRequest) MarshalJSON() ([]byte, error) {
type Alias GetTradeBillRequest
a := &struct {
BillDate *string `json:"bill_date,omitempty"`
SubMchid *string `json:"sub_mchid,omitempty"`
BillType *BillType `json:"bill_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
*Alias
}{
BillDate: nil,
SubMchid: nil,
BillType: nil,
TarType: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type QueryBillEntity struct {
HashType *HashType `json:"hash_type,omitempty"`
HashValue *string `json:"hash_value,omitempty"`
DownloadUrl *string `json:"download_url,omitempty"`
}
type BillType string
func (e BillType) Ptr() *BillType {
return &e
}
const (
BILLTYPE_ALL BillType = "ALL"
BILLTYPE_SUCCESS BillType = "SUCCESS"
BILLTYPE_REFUND BillType = "REFUND"
)
type TarType string
func (e TarType) Ptr() *TarType {
return &e
}
const (
TARTYPE_GZIP TarType = "GZIP"
)
type HashType string
func (e HashType) Ptr() *HashType {
return &e
}
const (
HASHTYPE_SHA1 HashType = "SHA1"
)

View File

@@ -0,0 +1,163 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/partner/4013080340
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &GetFundFlowBillRequest{
BillDate: wxpay_utility.String("2019-06-11"),
AccountType: FUNDFLOWBILLACCOUNTTYPE_BASIC.Ptr(),
TarType: TARTYPE_GZIP.Ptr(),
}
response, err := GetFundFlowBill(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func GetFundFlowBill(config *wxpay_utility.MchConfig, request *GetFundFlowBillRequest) (response *QueryBillEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/bill/fundflowbill"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
query := reqUrl.Query()
query.Add("bill_date", *request.BillDate)
query.Add("account_type", fmt.Sprintf("%v", *request.AccountType))
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &QueryBillEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type GetFundFlowBillRequest struct {
BillDate *string `json:"bill_date,omitempty"`
AccountType *FundFlowBillAccountType `json:"account_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
}
func (o *GetFundFlowBillRequest) MarshalJSON() ([]byte, error) {
type Alias GetFundFlowBillRequest
a := &struct {
BillDate *string `json:"bill_date,omitempty"`
AccountType *FundFlowBillAccountType `json:"account_type,omitempty"`
TarType *TarType `json:"tar_type,omitempty"`
*Alias
}{
BillDate: nil,
AccountType: nil,
TarType: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type QueryBillEntity struct {
HashType *HashType `json:"hash_type,omitempty"`
HashValue *string `json:"hash_value,omitempty"`
DownloadUrl *string `json:"download_url,omitempty"`
}
type FundFlowBillAccountType string
func (e FundFlowBillAccountType) Ptr() *FundFlowBillAccountType {
return &e
}
const (
FUNDFLOWBILLACCOUNTTYPE_BASIC FundFlowBillAccountType = "BASIC"
FUNDFLOWBILLACCOUNTTYPE_OPERATION FundFlowBillAccountType = "OPERATION"
FUNDFLOWBILLACCOUNTTYPE_FEES FundFlowBillAccountType = "FEES"
)
type TarType string
func (e TarType) Ptr() *TarType {
return &e
}
const (
TARTYPE_GZIP TarType = "GZIP"
)
type HashType string
func (e HashType) Ptr() *HashType {
return &e
}
const (
HASHTYPE_SHA1 HashType = "SHA1"
)

View File

@@ -0,0 +1,123 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/partner/4013080340
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &ChangeCustomPageStatusRequest{
SubMchid: wxpay_utility.String("1900000109"),
OperationType: OPERATIONTYPE_OPEN.Ptr(),
}
response, err := ChangeCustomPageStatus(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func ChangeCustomPageStatus(config *wxpay_utility.MchConfig, request *ChangeCustomPageStatusRequest) (response *ChangeCustomPageStatusResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/goldplan/merchants/changecustompagestatus"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &ChangeCustomPageStatusResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type ChangeCustomPageStatusRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
OperationType *OperationType `json:"operation_type,omitempty"`
}
type ChangeCustomPageStatusResponse struct {
SubMchid *string `json:"sub_mchid,omitempty"`
}
type OperationType string
func (e OperationType) Ptr() *OperationType {
return &e
}
const (
OPERATIONTYPE_OPEN OperationType = "OPEN"
OPERATIONTYPE_CLOSE OperationType = "CLOSE"
)

View File

@@ -0,0 +1,137 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
// TODO: 请准备商户开发必要参数参考https://pay.weixin.qq.com/doc/v3/partner/4013080340
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/partner/4013080340
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013058924
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径本地文件路径
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID如何获取请参考 https://pay.weixin.qq.com/doc/v3/partner/4013038589
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
)
if err != nil {
fmt.Println(err)
return
}
request := &ChangeGoldPlanStatusRequest{
SubMchid: wxpay_utility.String("1900000109"),
OperationType: OPERATIONTYPE_OPEN.Ptr(),
OperationPayScene: OPERATIONPAYSCENE_JSAPI_AND_MINIPROGRAM.Ptr(),
}
response, err := ChangeGoldPlanStatus(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
// TODO: 请求失败,根据状态码执行不同的处理
return
}
// TODO: 请求成功,继续业务逻辑
fmt.Printf("请求成功: %+v\n", response)
}
func ChangeGoldPlanStatus(config *wxpay_utility.MchConfig, request *ChangeGoldPlanStatusRequest) (response *ChangeGoldPlanStatusResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/goldplan/merchants/changegoldplanstatus"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = wxpay_utility.ValidateResponse(
config.WechatPayPublicKeyId(),
config.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
response := &ChangeGoldPlanStatusResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(
httpResponse.StatusCode,
httpResponse.Header,
respBody,
)
}
}
type ChangeGoldPlanStatusRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
OperationType *OperationType `json:"operation_type,omitempty"`
OperationPayScene *OperationPayScene `json:"operation_pay_scene,omitempty"`
}
type ChangeGoldPlanStatusResponse struct {
SubMchid *string `json:"sub_mchid,omitempty"`
}
type OperationType string
func (e OperationType) Ptr() *OperationType {
return &e
}
const (
OPERATIONTYPE_OPEN OperationType = "OPEN"
OPERATIONTYPE_CLOSE OperationType = "CLOSE"
)
type OperationPayScene string
func (e OperationPayScene) Ptr() *OperationPayScene {
return &e
}
const (
OPERATIONPAYSCENE_JSAPI_AND_MINIPROGRAM OperationPayScene = "JSAPI_AND_MINIPROGRAM"
OPERATIONPAYSCENE_JSAPI OperationPayScene = "JSAPI"
OPERATIONPAYSCENE_MINIPROGRAM OperationPayScene = "MINIPROGRAM"
)

View File

@@ -0,0 +1,88 @@
package main
import (
"bytes"
"demo/wxpay_utility"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &CloseAdvertisingShowRequest{
SubMchid: wxpay_utility.String("1900000109"),
}
err = CloseAdvertisingShow(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Println("请求成功")
}
func CloseAdvertisingShow(config *wxpay_utility.MchConfig, request *CloseAdvertisingShowRequest) (err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/goldplan/merchants/close-advertising-show"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return err
}
reqBody, err := json.Marshal(request)
if err != nil {
return err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return err
}
return nil
} else {
return wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type CloseAdvertisingShowRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
}

View File

@@ -0,0 +1,133 @@
package main
import (
"bytes"
"demo/wxpay_utility"
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &OpenAdvertisingShowRequest{
SubMchid: wxpay_utility.String("1900000109"),
AdvertisingIndustryFilters: []IndustryType{INDUSTRYTYPE_E_COMMERCE},
}
err = OpenAdvertisingShow(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Println("请求成功")
}
func OpenAdvertisingShow(config *wxpay_utility.MchConfig, request *OpenAdvertisingShowRequest) (err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "PATCH"
path = "/v3/goldplan/merchants/open-advertising-show"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return err
}
reqBody, err := json.Marshal(request)
if err != nil {
return err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return err
}
return nil
} else {
return wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type OpenAdvertisingShowRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
AdvertisingIndustryFilters []IndustryType `json:"advertising_industry_filters,omitempty"`
}
type IndustryType string
func (e IndustryType) Ptr() *IndustryType { return &e }
const (
INDUSTRYTYPE_E_COMMERCE IndustryType = "E_COMMERCE"
INDUSTRYTYPE_LOVE_MARRIAGE IndustryType = "LOVE_MARRIAGE"
INDUSTRYTYPE_POTOGRAPHY IndustryType = "POTOGRAPHY"
INDUSTRYTYPE_EDUCATION IndustryType = "EDUCATION"
INDUSTRYTYPE_FINANCE IndustryType = "FINANCE"
INDUSTRYTYPE_TOURISM IndustryType = "TOURISM"
INDUSTRYTYPE_SKINCARE IndustryType = "SKINCARE"
INDUSTRYTYPE_FOOD IndustryType = "FOOD"
INDUSTRYTYPE_SPORT IndustryType = "SPORT"
INDUSTRYTYPE_JEWELRY_WATCH IndustryType = "JEWELRY_WATCH"
INDUSTRYTYPE_HEALTHCARE IndustryType = "HEALTHCARE"
INDUSTRYTYPE_BUSSINESS IndustryType = "BUSSINESS"
INDUSTRYTYPE_PARENTING IndustryType = "PARENTING"
INDUSTRYTYPE_CATERING IndustryType = "CATERING"
INDUSTRYTYPE_RETAIL IndustryType = "RETAIL"
INDUSTRYTYPE_SERVICES IndustryType = "SERVICES"
INDUSTRYTYPE_LAW IndustryType = "LAW"
INDUSTRYTYPE_ESTATE IndustryType = "ESTATE"
INDUSTRYTYPE_TRANSPORTATION IndustryType = "TRANSPORTATION"
INDUSTRYTYPE_ENERGY_SAVING IndustryType = "ENERGY_SAVING"
INDUSTRYTYPE_SECURITY IndustryType = "SECURITY"
INDUSTRYTYPE_BUILDING_MATERIAL IndustryType = "BUILDING_MATERIAL"
INDUSTRYTYPE_COMMUNICATION IndustryType = "COMMUNICATION"
INDUSTRYTYPE_MERCHANDISE IndustryType = "MERCHANDISE"
INDUSTRYTYPE_ASSOCIATION IndustryType = "ASSOCIATION"
INDUSTRYTYPE_COMMUNITY IndustryType = "COMMUNITY"
INDUSTRYTYPE_ONLINE_AVR IndustryType = "ONLINE_AVR"
INDUSTRYTYPE_WE_MEDIA IndustryType = "WE_MEDIA"
INDUSTRYTYPE_CAR IndustryType = "CAR"
INDUSTRYTYPE_SOFTWARE IndustryType = "SOFTWARE"
INDUSTRYTYPE_GAME IndustryType = "GAME"
INDUSTRYTYPE_CLOTHING IndustryType = "CLOTHING"
INDUSTRYTYPE_INDUSTY IndustryType = "INDUSTY"
INDUSTRYTYPE_AGRICULTURE IndustryType = "AGRICULTURE"
INDUSTRYTYPE_PUBLISHING_MEDIA IndustryType = "PUBLISHING_MEDIA"
INDUSTRYTYPE_HOME_DIGITAL IndustryType = "HOME_DIGITAL"
)

View File

@@ -0,0 +1,133 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &SetAdvertisingIndustryFilterRequest{
SubMchid: wxpay_utility.String("1900000109"),
AdvertisingIndustryFilters: []IndustryType{INDUSTRYTYPE_E_COMMERCE},
}
err = SetAdvertisingIndustryFilter(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Println("请求成功")
}
func SetAdvertisingIndustryFilter(config *wxpay_utility.MchConfig, request *SetAdvertisingIndustryFilterRequest) (err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/goldplan/merchants/set-advertising-industry-filter"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return err
}
reqBody, err := json.Marshal(request)
if err != nil {
return err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return err
}
return nil
} else {
return wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type SetAdvertisingIndustryFilterRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
AdvertisingIndustryFilters []IndustryType `json:"advertising_industry_filters,omitempty"`
}
type IndustryType string
func (e IndustryType) Ptr() *IndustryType { return &e }
const (
INDUSTRYTYPE_E_COMMERCE IndustryType = "E_COMMERCE"
INDUSTRYTYPE_LOVE_MARRIAGE IndustryType = "LOVE_MARRIAGE"
INDUSTRYTYPE_POTOGRAPHY IndustryType = "POTOGRAPHY"
INDUSTRYTYPE_EDUCATION IndustryType = "EDUCATION"
INDUSTRYTYPE_FINANCE IndustryType = "FINANCE"
INDUSTRYTYPE_TOURISM IndustryType = "TOURISM"
INDUSTRYTYPE_SKINCARE IndustryType = "SKINCARE"
INDUSTRYTYPE_FOOD IndustryType = "FOOD"
INDUSTRYTYPE_SPORT IndustryType = "SPORT"
INDUSTRYTYPE_JEWELRY_WATCH IndustryType = "JEWELRY_WATCH"
INDUSTRYTYPE_HEALTHCARE IndustryType = "HEALTHCARE"
INDUSTRYTYPE_BUSSINESS IndustryType = "BUSSINESS"
INDUSTRYTYPE_PARENTING IndustryType = "PARENTING"
INDUSTRYTYPE_CATERING IndustryType = "CATERING"
INDUSTRYTYPE_RETAIL IndustryType = "RETAIL"
INDUSTRYTYPE_SERVICES IndustryType = "SERVICES"
INDUSTRYTYPE_LAW IndustryType = "LAW"
INDUSTRYTYPE_ESTATE IndustryType = "ESTATE"
INDUSTRYTYPE_TRANSPORTATION IndustryType = "TRANSPORTATION"
INDUSTRYTYPE_ENERGY_SAVING IndustryType = "ENERGY_SAVING"
INDUSTRYTYPE_SECURITY IndustryType = "SECURITY"
INDUSTRYTYPE_BUILDING_MATERIAL IndustryType = "BUILDING_MATERIAL"
INDUSTRYTYPE_COMMUNICATION IndustryType = "COMMUNICATION"
INDUSTRYTYPE_MERCHANDISE IndustryType = "MERCHANDISE"
INDUSTRYTYPE_ASSOCIATION IndustryType = "ASSOCIATION"
INDUSTRYTYPE_COMMUNITY IndustryType = "COMMUNITY"
INDUSTRYTYPE_ONLINE_AVR IndustryType = "ONLINE_AVR"
INDUSTRYTYPE_WE_MEDIA IndustryType = "WE_MEDIA"
INDUSTRYTYPE_CAR IndustryType = "CAR"
INDUSTRYTYPE_SOFTWARE IndustryType = "SOFTWARE"
INDUSTRYTYPE_GAME IndustryType = "GAME"
INDUSTRYTYPE_CLOTHING IndustryType = "CLOTHING"
INDUSTRYTYPE_INDUSTY IndustryType = "INDUSTY"
INDUSTRYTYPE_AGRICULTURE IndustryType = "AGRICULTURE"
INDUSTRYTYPE_PUBLISHING_MEDIA IndustryType = "PUBLISHING_MEDIA"
INDUSTRYTYPE_HOME_DIGITAL IndustryType = "HOME_DIGITAL"
)

View File

@@ -0,0 +1,142 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &AddReceiverRequest{
SubMchid: wxpay_utility.String("1900000109"),
Appid: wxpay_utility.String("wx8888888888888888"),
SubAppid: wxpay_utility.String("wx8888888888888889"),
Type: RECEIVERTYPE_MERCHANT_ID.Ptr(),
Account: wxpay_utility.String("86693852"),
Name: wxpay_utility.String("hu89ohu89ohu89o"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
RelationType: RECEIVERRELATIONTYPE_STORE.Ptr(),
CustomRelation: wxpay_utility.String("代理商"),
}
response, err := AddReceiver(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func AddReceiver(config *wxpay_utility.MchConfig, request *AddReceiverRequest) (response *AddReceiverResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/receivers/add"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &AddReceiverResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type AddReceiverRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
Appid *string `json:"appid,omitempty"`
SubAppid *string `json:"sub_appid,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
RelationType *ReceiverRelationType `json:"relation_type,omitempty"`
CustomRelation *string `json:"custom_relation,omitempty"`
}
type AddReceiverResponse struct {
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
RelationType *ReceiverRelationType `json:"relation_type,omitempty"`
CustomRelation *string `json:"custom_relation,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType { return &e }
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
RECEIVERTYPE_PERSONAL_SUB_OPENID ReceiverType = "PERSONAL_SUB_OPENID"
)
type ReceiverRelationType string
func (e ReceiverRelationType) Ptr() *ReceiverRelationType { return &e }
const (
RECEIVERRELATIONTYPE_STORE ReceiverRelationType = "STORE"
RECEIVERRELATIONTYPE_STAFF ReceiverRelationType = "STAFF"
RECEIVERRELATIONTYPE_STORE_OWNER ReceiverRelationType = "STORE_OWNER"
RECEIVERRELATIONTYPE_PARTNER ReceiverRelationType = "PARTNER"
RECEIVERRELATIONTYPE_HEADQUARTER ReceiverRelationType = "HEADQUARTER"
RECEIVERRELATIONTYPE_BRAND ReceiverRelationType = "BRAND"
RECEIVERRELATIONTYPE_DISTRIBUTOR ReceiverRelationType = "DISTRIBUTOR"
RECEIVERRELATIONTYPE_USER ReceiverRelationType = "USER"
RECEIVERRELATIONTYPE_SUPPLIER ReceiverRelationType = "SUPPLIER"
RECEIVERRELATIONTYPE_CUSTOM ReceiverRelationType = "CUSTOM"
RECEIVERRELATIONTYPE_SERVICE_PROVIDER ReceiverRelationType = "SERVICE_PROVIDER"
)

View File

@@ -0,0 +1,184 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &CreateOrderRequest{
SubMchid: wxpay_utility.String("1900000109"),
Appid: wxpay_utility.String("wx8888888888888888"),
SubAppid: wxpay_utility.String("wx8888888888888889"),
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
OutOrderNo: wxpay_utility.String("P20150806125346"),
Receivers: []CreateOrderReceiver{CreateOrderReceiver{
Type: wxpay_utility.String("MERCHANT_ID"),
Account: wxpay_utility.String("86693852"),
Name: wxpay_utility.String("hu89ohu89ohu89o"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
Amount: wxpay_utility.Int64(888),
Description: wxpay_utility.String("分给商户A"),
}},
UnfreezeUnsplit: wxpay_utility.Bool(true),
}
response, err := CreateOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func CreateOrder(config *wxpay_utility.MchConfig, request *CreateOrderRequest) (response *OrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/orders"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &OrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type CreateOrderRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
Appid *string `json:"appid,omitempty"`
SubAppid *string `json:"sub_appid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
Receivers []CreateOrderReceiver `json:"receivers,omitempty"`
UnfreezeUnsplit *bool `json:"unfreeze_unsplit,omitempty"`
}
type OrdersEntity struct {
SubMchid *string `json:"sub_mchid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OrderId *string `json:"order_id,omitempty"`
State *OrderStatus `json:"state,omitempty"`
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
}
type CreateOrderReceiver struct {
Type *string `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Name *string `json:"name,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
}
type OrderStatus string
func (e OrderStatus) Ptr() *OrderStatus { return &e }
const (
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
)
type OrderReceiverDetail struct {
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Result *DetailStatus `json:"result,omitempty"`
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
DetailId *string `json:"detail_id,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType { return &e }
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
RECEIVERTYPE_PERSONAL_SUB_OPENID ReceiverType = "PERSONAL_SUB_OPENID"
)
type DetailStatus string
func (e DetailStatus) Ptr() *DetailStatus { return &e }
const (
DETAILSTATUS_PENDING DetailStatus = "PENDING"
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
)
type DetailFailReason string
func (e DetailFailReason) Ptr() *DetailFailReason { return &e }
const (
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
)

View File

@@ -0,0 +1,142 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &CreateReturnOrderRequest{
SubMchid: wxpay_utility.String("1900000109"),
OrderId: wxpay_utility.String("3008450740201411110007820472"),
OutOrderNo: wxpay_utility.String("P20150806125346"),
OutReturnNo: wxpay_utility.String("R20190516001"),
ReturnMchid: wxpay_utility.String("86693852"),
Amount: wxpay_utility.Int64(10),
Description: wxpay_utility.String("用户退款"),
}
response, err := CreateReturnOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func CreateReturnOrder(config *wxpay_utility.MchConfig, request *CreateReturnOrderRequest) (response *ReturnOrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/return-orders"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &ReturnOrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type CreateReturnOrderRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
OrderId *string `json:"order_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OutReturnNo *string `json:"out_return_no,omitempty"`
ReturnMchid *string `json:"return_mchid,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
}
type ReturnOrdersEntity struct {
SubMchid *string `json:"sub_mchid,omitempty"`
OrderId *string `json:"order_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OutReturnNo *string `json:"out_return_no,omitempty"`
ReturnId *string `json:"return_id,omitempty"`
ReturnMchid *string `json:"return_mchid,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Result *ReturnOrderStatus `json:"result,omitempty"`
FailReason *ReturnOrderFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
}
type ReturnOrderStatus string
func (e ReturnOrderStatus) Ptr() *ReturnOrderStatus { return &e }
const (
RETURNORDERSTATUS_PROCESSING ReturnOrderStatus = "PROCESSING"
RETURNORDERSTATUS_SUCCESS ReturnOrderStatus = "SUCCESS"
RETURNORDERSTATUS_FAILED ReturnOrderStatus = "FAILED"
)
type ReturnOrderFailReason string
func (e ReturnOrderFailReason) Ptr() *ReturnOrderFailReason { return &e }
const (
RETURNORDERFAILREASON_ACCOUNT_ABNORMAL ReturnOrderFailReason = "ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_BALANCE_NOT_ENOUGH ReturnOrderFailReason = "BALANCE_NOT_ENOUGH"
RETURNORDERFAILREASON_TIME_OUT_CLOSED ReturnOrderFailReason = "TIME_OUT_CLOSED"
RETURNORDERFAILREASON_PAYER_ACCOUNT_ABNORMAL ReturnOrderFailReason = "PAYER_ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_INVALID_REQUEST ReturnOrderFailReason = "INVALID_REQUEST"
)

View File

@@ -0,0 +1,115 @@
package main
import (
"bytes"
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &DeleteReceiverRequest{
SubMchid: wxpay_utility.String("1900000109"),
Appid: wxpay_utility.String("wx8888888888888888"),
SubAppid: wxpay_utility.String("wx8888888888888889"),
Type: RECEIVERTYPE_MERCHANT_ID.Ptr(),
Account: wxpay_utility.String("1900000109"),
}
response, err := DeleteReceiver(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func DeleteReceiver(config *wxpay_utility.MchConfig, request *DeleteReceiverRequest) (response *DeleteReceiverResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "POST"
path = "/v3/profitsharing/receivers/delete"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqBody, err := json.Marshal(request)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &DeleteReceiverResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type DeleteReceiverRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
Appid *string `json:"appid,omitempty"`
SubAppid *string `json:"sub_appid,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
}
type DeleteReceiverResponse struct {
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType { return &e }
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
RECEIVERTYPE_PERSONAL_SUB_OPENID ReceiverType = "PERSONAL_SUB_OPENID"
)

View File

@@ -0,0 +1,105 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryMerchantRatioRequest{
SubMchid: wxpay_utility.String("1900000109"),
}
response, err := QueryMerchantRatio(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func QueryMerchantRatio(config *wxpay_utility.MchConfig, request *QueryMerchantRatioRequest) (response *QueryMerchantRatioResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/merchant-configs/{sub_mchid}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{sub_mchid}", url.PathEscape(*request.SubMchid), -1)
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &QueryMerchantRatioResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type QueryMerchantRatioRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
}
func (o *QueryMerchantRatioRequest) MarshalJSON() ([]byte, error) {
type Alias QueryMerchantRatioRequest
a := &struct {
SubMchid *string `json:"sub_mchid,omitempty"`
*Alias
}{
SubMchid: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type QueryMerchantRatioResponse struct {
SubMchid *string `json:"sub_mchid,omitempty"`
MaxRatio *int64 `json:"max_ratio,omitempty"`
}

View File

@@ -0,0 +1,182 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryOrderRequest{
SubMchid: wxpay_utility.String("1900000109"),
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
OutOrderNo: wxpay_utility.String("P20150806125346"),
}
response, err := QueryOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func QueryOrder(config *wxpay_utility.MchConfig, request *QueryOrderRequest) (response *OrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/orders/{out_order_no}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_order_no}", url.PathEscape(*request.OutOrderNo), -1)
query := reqUrl.Query()
if request.SubMchid != nil {
query.Add("sub_mchid", *request.SubMchid)
}
if request.TransactionId != nil {
query.Add("transaction_id", *request.TransactionId)
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &OrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type QueryOrderRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
}
func (o *QueryOrderRequest) MarshalJSON() ([]byte, error) {
type Alias QueryOrderRequest
a := &struct {
SubMchid *string `json:"sub_mchid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
*Alias
}{
SubMchid: nil,
TransactionId: nil,
OutOrderNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type OrdersEntity struct {
SubMchid *string `json:"sub_mchid,omitempty"`
TransactionId *string `json:"transaction_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OrderId *string `json:"order_id,omitempty"`
State *OrderStatus `json:"state,omitempty"`
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
}
type OrderStatus string
func (e OrderStatus) Ptr() *OrderStatus { return &e }
const (
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
)
type OrderReceiverDetail struct {
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Type *ReceiverType `json:"type,omitempty"`
Account *string `json:"account,omitempty"`
Result *DetailStatus `json:"result,omitempty"`
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
DetailId *string `json:"detail_id,omitempty"`
}
type ReceiverType string
func (e ReceiverType) Ptr() *ReceiverType { return &e }
const (
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
RECEIVERTYPE_PERSONAL_SUB_OPENID ReceiverType = "PERSONAL_SUB_OPENID"
)
type DetailStatus string
func (e DetailStatus) Ptr() *DetailStatus { return &e }
const (
DETAILSTATUS_PENDING DetailStatus = "PENDING"
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
)
type DetailFailReason string
func (e DetailFailReason) Ptr() *DetailFailReason { return &e }
const (
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
)

View File

@@ -0,0 +1,105 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryOrderAmountRequest{
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
}
response, err := QueryOrderAmount(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func QueryOrderAmount(config *wxpay_utility.MchConfig, request *QueryOrderAmountRequest) (response *QueryOrderAmountResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/transactions/{transaction_id}/amounts"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{transaction_id}", url.PathEscape(*request.TransactionId), -1)
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &QueryOrderAmountResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type QueryOrderAmountRequest struct {
TransactionId *string `json:"transaction_id,omitempty"`
}
func (o *QueryOrderAmountRequest) MarshalJSON() ([]byte, error) {
type Alias QueryOrderAmountRequest
a := &struct {
TransactionId *string `json:"transaction_id,omitempty"`
*Alias
}{
TransactionId: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type QueryOrderAmountResponse struct {
TransactionId *string `json:"transaction_id,omitempty"`
UnsplitAmount *int64 `json:"unsplit_amount,omitempty"`
}

View File

@@ -0,0 +1,154 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &QueryReturnOrderRequest{
OutReturnNo: wxpay_utility.String("R20190516001"),
SubMchid: wxpay_utility.String("1900000109"),
OutOrderNo: wxpay_utility.String("P20190806125346"),
}
response, err := QueryReturnOrder(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func QueryReturnOrder(config *wxpay_utility.MchConfig, request *QueryReturnOrderRequest) (response *ReturnOrdersEntity, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/return-orders/{out_return_no}"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_return_no}", url.PathEscape(*request.OutReturnNo), -1)
query := reqUrl.Query()
if request.SubMchid != nil {
query.Add("sub_mchid", *request.SubMchid)
}
if request.OutOrderNo != nil {
query.Add("out_order_no", *request.OutOrderNo)
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &ReturnOrdersEntity{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type QueryReturnOrderRequest struct {
OutReturnNo *string `json:"out_return_no,omitempty"`
SubMchid *string `json:"sub_mchid,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
}
func (o *QueryReturnOrderRequest) MarshalJSON() ([]byte, error) {
type Alias QueryReturnOrderRequest
a := &struct {
OutReturnNo *string `json:"out_return_no,omitempty"`
SubMchid *string `json:"sub_mchid,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
*Alias
}{
OutReturnNo: nil,
SubMchid: nil,
OutOrderNo: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type ReturnOrdersEntity struct {
SubMchid *string `json:"sub_mchid,omitempty"`
OrderId *string `json:"order_id,omitempty"`
OutOrderNo *string `json:"out_order_no,omitempty"`
OutReturnNo *string `json:"out_return_no,omitempty"`
ReturnId *string `json:"return_id,omitempty"`
ReturnMchid *string `json:"return_mchid,omitempty"`
Amount *int64 `json:"amount,omitempty"`
Description *string `json:"description,omitempty"`
Result *ReturnOrderStatus `json:"result,omitempty"`
FailReason *ReturnOrderFailReason `json:"fail_reason,omitempty"`
CreateTime *time.Time `json:"create_time,omitempty"`
FinishTime *time.Time `json:"finish_time,omitempty"`
}
type ReturnOrderStatus string
func (e ReturnOrderStatus) Ptr() *ReturnOrderStatus { return &e }
const (
RETURNORDERSTATUS_PROCESSING ReturnOrderStatus = "PROCESSING"
RETURNORDERSTATUS_SUCCESS ReturnOrderStatus = "SUCCESS"
RETURNORDERSTATUS_FAILED ReturnOrderStatus = "FAILED"
)
type ReturnOrderFailReason string
func (e ReturnOrderFailReason) Ptr() *ReturnOrderFailReason { return &e }
const (
RETURNORDERFAILREASON_ACCOUNT_ABNORMAL ReturnOrderFailReason = "ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_BALANCE_NOT_ENOUGH ReturnOrderFailReason = "BALANCE_NOT_ENOUGH"
RETURNORDERFAILREASON_TIME_OUT_CLOSED ReturnOrderFailReason = "TIME_OUT_CLOSED"
RETURNORDERFAILREASON_PAYER_ACCOUNT_ABNORMAL ReturnOrderFailReason = "PAYER_ACCOUNT_ABNORMAL"
RETURNORDERFAILREASON_INVALID_REQUEST ReturnOrderFailReason = "INVALID_REQUEST"
)

View File

@@ -0,0 +1,139 @@
package main
import (
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
"encoding/json"
"fmt"
"net/http"
"net/url"
)
func main() {
config, err := wxpay_utility.CreateMchConfig(
"19xxxxxxxx",
"1DDE55AD98Exxxxxxxxxx",
"/path/to/apiclient_key.pem",
"PUB_KEY_ID_xxxxxxxxxxxxx",
"/path/to/wxp_pub.pem",
)
if err != nil {
fmt.Println(err)
return
}
request := &SplitBillRequest{
SubMchid: wxpay_utility.String("1900000109"),
BillDate: wxpay_utility.String("2019-06-11"),
TarType: SPLITBILLTARTYPE_GZIP.Ptr(),
}
response, err := SplitBill(config, request)
if err != nil {
fmt.Printf("请求失败: %+v\n", err)
return
}
fmt.Printf("请求成功: %+v\n", response)
}
func SplitBill(config *wxpay_utility.MchConfig, request *SplitBillRequest) (response *SplitBillResponse, err error) {
const (
host = "https://api.mch.weixin.qq.com"
method = "GET"
path = "/v3/profitsharing/bills"
)
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
query := reqUrl.Query()
if request.SubMchid != nil {
query.Add("sub_mchid", *request.SubMchid)
}
if request.BillDate != nil {
query.Add("bill_date", *request.BillDate)
}
if request.TarType != nil {
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
}
reqUrl.RawQuery = query.Encode()
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
if err != nil {
return nil, err
}
response := &SplitBillResponse{}
if err := json.Unmarshal(respBody, response); err != nil {
return nil, err
}
return response, nil
} else {
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
}
}
type SplitBillRequest struct {
SubMchid *string `json:"sub_mchid,omitempty"`
BillDate *string `json:"bill_date,omitempty"`
TarType *SplitBillTarType `json:"tar_type,omitempty"`
}
func (o *SplitBillRequest) MarshalJSON() ([]byte, error) {
type Alias SplitBillRequest
a := &struct {
SubMchid *string `json:"sub_mchid,omitempty"`
BillDate *string `json:"bill_date,omitempty"`
TarType *SplitBillTarType `json:"tar_type,omitempty"`
*Alias
}{
SubMchid: nil,
BillDate: nil,
TarType: nil,
Alias: (*Alias)(o),
}
return json.Marshal(a)
}
type SplitBillResponse struct {
HashType *SplitBillHashType `json:"hash_type,omitempty"`
HashValue *string `json:"hash_value,omitempty"`
DownloadUrl *string `json:"download_url,omitempty"`
}
type SplitBillTarType string
func (e SplitBillTarType) Ptr() *SplitBillTarType { return &e }
const (
SPLITBILLTARTYPE_GZIP SplitBillTarType = "GZIP"
)
type SplitBillHashType string
func (e SplitBillHashType) Ptr() *SplitBillHashType { return &e }
const (
SPLITBILLHASHTYPE_SHA1 SplitBillHashType = "SHA1"
)

Some files were not shown because too many files have changed in this diff Show More