Compare commits
139 Commits
b2ac92e0fc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7972661d1e | |||
| 6672867c6f | |||
| 2eded08bc7 | |||
| 3288b6aafb | |||
| c9a4176a41 | |||
| 74fd9a33ac | |||
| a30f296cdc | |||
| 73424f958a | |||
| f31bb7e7e5 | |||
| ed8c93fb5d | |||
| 8ade75390c | |||
| 2801b55d2f | |||
| b24af5a279 | |||
| 4642855fd0 | |||
| cf3dcc6195 | |||
| bca439726d | |||
| 548db78ca7 | |||
| 5c5a8d4a40 | |||
| 514365fdec | |||
| de25324991 | |||
| 1c35662ed5 | |||
| 379ce60839 | |||
| 4f36235f60 | |||
| e55c12b68b | |||
| 502811a103 | |||
| 1d7ef7e4b6 | |||
| cb794601be | |||
| 4ba1ebbbdf | |||
| 332f887c66 | |||
| e444266e1e | |||
| 10e8beea80 | |||
| d33c937ebc | |||
| 7a75f5d612 | |||
| 2dc9d752e4 | |||
| 166544fae6 | |||
| e8648e45fc | |||
| 5a55180b78 | |||
| ae58a443a3 | |||
| bf4423e53b | |||
| 57de9a8df6 | |||
| c1131e6f55 | |||
| 2a75a19ece | |||
| 5b96265c50 | |||
| 2277b37888 | |||
| be53a90f77 | |||
| bcd7617fb7 | |||
| 49468441bc | |||
| a92dc2b7b0 | |||
| 4fecf9c975 | |||
| c3fbf7a30b | |||
| b13870f71b | |||
| e4a8bd42bb | |||
| 01c5ab985a | |||
| ac12f1ed5e | |||
| e36a562098 | |||
| 36e134e323 | |||
| 26139f80d3 | |||
| 9b72dbb3ea | |||
| 188c6704db | |||
| d641840098 | |||
| aec9142481 | |||
| d41f260a2a | |||
| cf074837a4 | |||
| ed7a6f48d0 | |||
| 8c6ec9e6e4 | |||
| 33c9079d3b | |||
| 7b4ba61b4d | |||
| ea0b67a951 | |||
| 4dfa8452db | |||
| 22810245f5 | |||
| eb76bfc031 | |||
| 183e78d475 | |||
| 612d105a23 | |||
| b994acf635 | |||
| ef4f91a75e | |||
| 481a27fc53 | |||
| 54968701f0 | |||
| 5cb5329f4e | |||
| 2b046656dc | |||
| 7cea41c911 | |||
| 928acb4302 | |||
| bf72c2e48d | |||
| e30b733b17 | |||
| fa61eeb0b0 | |||
| 2ca096f821 | |||
| 2b6087de4c | |||
| fda996031f | |||
| 10ed4fa051 | |||
| ac2cf78ffa | |||
| 5edfb756c7 | |||
| 0c9254502c | |||
| 0461c0ee41 | |||
| 81f57ea5ce | |||
| d23cf3807d | |||
| 6c1579a786 | |||
| 793d82cccd | |||
| 5cc8293380 | |||
| 85ed8ca90c | |||
| d0a9348e72 | |||
| 192accd796 | |||
| 54c2d6de47 | |||
| 7f2461313e | |||
| f74717c415 | |||
| 75bca28191 | |||
| 46a254f142 | |||
| 86fc382413 | |||
| 643161a168 | |||
| d6219f1a0c | |||
| 35d63f5b2e | |||
| 1c16152708 | |||
| f6084d0910 | |||
| 6ed6859855 | |||
|
|
9b39a52049 | ||
|
|
fc54bff62f | ||
| 1767bed609 | |||
| dada5a4797 | |||
| 7e608d4230 | |||
| 3ad1075227 | |||
| 32a1530ab1 | |||
| 7c8aa1e124 | |||
| 641d91cf11 | |||
| 052dbc248b | |||
| bc704d0c22 | |||
| a0ed128bde | |||
| 80a4183b45 | |||
| 8669a996ca | |||
| 9ca66715a4 | |||
| e390b72a0c | |||
| cf9fb5ac40 | |||
| a1e5c2150c | |||
| 23ba2703b4 | |||
| 96df12cd15 | |||
| 65c2b8cd79 | |||
| 199b44c18c | |||
| e410f7974e | |||
| 94975e4735 | |||
| abf1f1ebea | |||
| ce98a29c4d | |||
| 9baa515a75 |
4
.codex/logs/run-dev-web-final.ps1
Normal file
4
.codex/logs/run-dev-web-final.ps1
Normal 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'
|
||||||
1
.codex/skills/behavior-driven-development
Normal file
1
.codex/skills/behavior-driven-development
Normal file
@@ -0,0 +1 @@
|
|||||||
|
C:/proj/Genarrative/.hermes/skills/behavior-driven-development
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
name: gpt-image-2-apimart
|
name: gpt-image-2-apimart
|
||||||
description: Generate or inspect project image assets through this repository's APIMart OpenAI-compatible 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 APIMART_BASE_URL / APIMART_API_KEY image-generation configuration without exposing secrets.
|
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 APIMart
|
# gpt-image-2 VectorEngine
|
||||||
|
|
||||||
Use this skill for project-local image asset generation that must match the repository's `server-rs` APIMart `gpt-image-2` path.
|
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
|
## Workflow
|
||||||
|
|
||||||
@@ -24,15 +24,15 @@ Use this skill for project-local image asset generation that must match the repo
|
|||||||
```
|
```
|
||||||
|
|
||||||
5. Save final project assets under `public/` or another explicitly requested workspace path.
|
5. Save final project assets under `public/` or another explicitly requested workspace path.
|
||||||
6. Never print `APIMART_API_KEY`. Report only whether configuration exists.
|
6. Never print `VECTOR_ENGINE_API_KEY`. Report only whether configuration exists.
|
||||||
|
|
||||||
## Request Contract
|
## Request Contract
|
||||||
|
|
||||||
The repository image path uses:
|
The repository image path uses:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
POST {APIMART_BASE_URL}/images/generations
|
POST {VECTOR_ENGINE_BASE_URL}/v1/images/generations
|
||||||
Authorization: Bearer {APIMART_API_KEY}
|
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,28 +40,42 @@ Default body:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model": "gpt-image-2",
|
"model": "gpt-image-2-all",
|
||||||
"prompt": "<prompt>",
|
"prompt": "<prompt>",
|
||||||
"n": 1,
|
"n": 1,
|
||||||
"size": "1:1"
|
"size": "1024x1024"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For a reference image, add:
|
For weak visual references in text-to-image generation, add:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"image_urls": ["data:image/png;base64,..."]
|
"image": ["data:image/png;base64,..."]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Poll async responses with:
|
For image-to-image work that must follow a reference image closely, use the VectorEngine edits endpoint instead of the generations `image` array:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
GET {APIMART_BASE_URL}/tasks/{task_id}
|
POST {VECTOR_ENGINE_BASE_URL}/v1/images/edits
|
||||||
|
Authorization: Bearer {VECTOR_ENGINE_API_KEY}
|
||||||
|
Content-Type: multipart/form-data
|
||||||
```
|
```
|
||||||
|
|
||||||
Accept image output from `data[].url`, `data[].b64_json`, direct nested `url` fields, or async task results.
|
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
|
## Environment
|
||||||
|
|
||||||
@@ -69,12 +83,12 @@ Load environment values from process env first, then `.env.secrets.local`, `.env
|
|||||||
|
|
||||||
Required for live generation:
|
Required for live generation:
|
||||||
|
|
||||||
- `APIMART_BASE_URL`
|
- `VECTOR_ENGINE_BASE_URL`
|
||||||
- `APIMART_API_KEY`
|
- `VECTOR_ENGINE_API_KEY`
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
|
|
||||||
- `APIMART_IMAGE_REQUEST_TIMEOUT_MS`
|
- `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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "GPT Image 2 APIMart"
|
display_name: "GPT Image 2 VectorEngine"
|
||||||
short_description: "Generate project thumbnails through APIMart"
|
short_description: "Generate project thumbnails through VectorEngine"
|
||||||
brand_color: "#10B981"
|
brand_color: "#10B981"
|
||||||
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through APIMart."
|
default_prompt: "Use $gpt-image-2-apimart to dry-run or generate puzzle template thumbnails through VectorEngine."
|
||||||
policy:
|
policy:
|
||||||
allow_implicit_invocation: true
|
allow_implicit_invocation: true
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -13,8 +13,7 @@ const promptsPath = path.join(
|
|||||||
'puzzle-template-prompts.json',
|
'puzzle-template-prompts.json',
|
||||||
);
|
);
|
||||||
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
|
const defaultOutDir = path.join(repoRoot, 'public', 'puzzle-creation-templates');
|
||||||
const defaultTimeoutMs = 180000;
|
const defaultTimeoutMs = 1000000;
|
||||||
const pollDelayMs = 3000;
|
|
||||||
|
|
||||||
const args = new Map();
|
const args = new Map();
|
||||||
for (let index = 2; index < process.argv.length; index += 1) {
|
for (let index = 2; index < process.argv.length; index += 1) {
|
||||||
@@ -66,15 +65,23 @@ function resolveEnv() {
|
|||||||
...process.env,
|
...process.env,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
baseUrl: String(loaded.APIMART_BASE_URL || '').trim().replace(/\/+$/u, ''),
|
baseUrl: String(loaded.VECTOR_ENGINE_BASE_URL || '')
|
||||||
apiKey: String(loaded.APIMART_API_KEY || '').trim(),
|
.trim()
|
||||||
|
.replace(/\/+$/u, ''),
|
||||||
|
apiKey: String(loaded.VECTOR_ENGINE_API_KEY || '').trim(),
|
||||||
timeoutMs: Number.parseInt(
|
timeoutMs: Number.parseInt(
|
||||||
String(loaded.APIMART_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
String(loaded.VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS || defaultTimeoutMs),
|
||||||
10,
|
10,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildVectorEngineImagesGenerationUrl(baseUrl) {
|
||||||
|
return baseUrl.endsWith('/v1')
|
||||||
|
? `${baseUrl}/images/generations`
|
||||||
|
: `${baseUrl}/v1/images/generations`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildPrompt(template) {
|
function buildPrompt(template) {
|
||||||
return [
|
return [
|
||||||
'请生成一张高清 1:1 方形插画,用作拼图创作模板样例图。',
|
'请生成一张高清 1:1 方形插画,用作拼图创作模板样例图。',
|
||||||
@@ -124,14 +131,6 @@ function extractBase64Images(payload) {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTaskId(payload) {
|
|
||||||
const ids = [];
|
|
||||||
collectStringsByKey(payload, 'task_id', ids);
|
|
||||||
collectStringsByKey(payload, 'taskId', ids);
|
|
||||||
collectStringsByKey(payload, 'id', ids);
|
|
||||||
return ids[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferExtensionFromContentType(contentType) {
|
function inferExtensionFromContentType(contentType) {
|
||||||
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
|
const normalized = contentType.split(';')[0]?.trim().toLowerCase();
|
||||||
if (normalized === 'image/png') {
|
if (normalized === 'image/png') {
|
||||||
@@ -172,9 +171,14 @@ async function fetchJson(url, options, timeoutMs) {
|
|||||||
});
|
});
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`APIMart ${response.status}: ${text.slice(0, 600)}`);
|
throw new Error(`VectorEngine ${response.status}: ${text.slice(0, 600)}`);
|
||||||
}
|
}
|
||||||
return JSON.parse(text);
|
return JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.name === 'AbortError') {
|
||||||
|
throw new Error(`VectorEngine request timed out after ${timeoutMs}ms`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
@@ -195,56 +199,30 @@ async function downloadUrl(url, timeoutMs) {
|
|||||||
response.headers.get('content-type') || 'image/jpeg',
|
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 {
|
} finally {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForTask(env, taskId) {
|
|
||||||
const deadline = Date.now() + env.timeoutMs;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
const payload = await fetchJson(
|
|
||||||
`${env.baseUrl}/tasks/${encodeURIComponent(taskId)}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${env.apiKey}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env.timeoutMs,
|
|
||||||
);
|
|
||||||
const statuses = [];
|
|
||||||
collectStringsByKey(payload, 'status', statuses);
|
|
||||||
collectStringsByKey(payload, 'task_status', statuses);
|
|
||||||
const status = String(statuses[0] || '').trim().toLowerCase();
|
|
||||||
|
|
||||||
if (['completed', 'succeeded', 'success'].includes(status)) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
if (['failed', 'error', 'canceled', 'cancelled', 'unknown'].includes(status)) {
|
|
||||||
throw new Error(`APIMart task ${taskId} failed: ${JSON.stringify(payload).slice(0, 600)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, pollDelayMs));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`APIMart task ${taskId} timed out`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateOne(env, template, outDir) {
|
async function generateOne(env, template, outDir) {
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: 'gpt-image-2',
|
model: 'gpt-image-2-all',
|
||||||
prompt: buildPrompt(template),
|
prompt: buildPrompt(template),
|
||||||
n: 1,
|
n: 1,
|
||||||
size: '1:1',
|
size: '1024x1024',
|
||||||
};
|
};
|
||||||
const payload = await fetchJson(
|
const payload = await fetchJson(
|
||||||
`${env.baseUrl}/images/generations`,
|
buildVectorEngineImagesGenerationUrl(env.baseUrl),
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${env.apiKey}`,
|
Authorization: `Bearer ${env.apiKey}`,
|
||||||
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
@@ -252,12 +230,8 @@ async function generateOne(env, template, outDir) {
|
|||||||
env.timeoutMs,
|
env.timeoutMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolvedPayload =
|
const urls = extractImageUrls(payload);
|
||||||
extractImageUrls(payload).length || extractBase64Images(payload).length
|
const b64Images = extractBase64Images(payload);
|
||||||
? payload
|
|
||||||
: await waitForTask(env, extractTaskId(payload));
|
|
||||||
const urls = extractImageUrls(resolvedPayload);
|
|
||||||
const b64Images = extractBase64Images(resolvedPayload);
|
|
||||||
|
|
||||||
let image;
|
let image;
|
||||||
if (urls[0]) {
|
if (urls[0]) {
|
||||||
@@ -269,7 +243,7 @@ async function generateOne(env, template, outDir) {
|
|||||||
extension: inferExtensionFromBytes(bytes),
|
extension: inferExtensionFromBytes(bytes),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`APIMart returned no image for ${template.id}`);
|
throw new Error(`VectorEngine returned no image for ${template.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(outDir, { recursive: true });
|
mkdirSync(outDir, { recursive: true });
|
||||||
@@ -301,10 +275,10 @@ if (dryRun) {
|
|||||||
id: template.id,
|
id: template.id,
|
||||||
title: template.title,
|
title: template.title,
|
||||||
body: {
|
body: {
|
||||||
model: 'gpt-image-2',
|
model: 'gpt-image-2-all',
|
||||||
prompt: buildPrompt(template),
|
prompt: buildPrompt(template),
|
||||||
n: 1,
|
n: 1,
|
||||||
size: '1:1',
|
size: '1024x1024',
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@@ -320,7 +294,7 @@ if (!env.baseUrl || !env.apiKey) {
|
|||||||
console.error(
|
console.error(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'Missing APIMART_BASE_URL or APIMART_API_KEY',
|
error: 'Missing VECTOR_ENGINE_BASE_URL or VECTOR_ENGINE_API_KEY',
|
||||||
hasBaseUrl: Boolean(env.baseUrl),
|
hasBaseUrl: Boolean(env.baseUrl),
|
||||||
hasApiKey: Boolean(env.apiKey),
|
hasApiKey: Boolean(env.apiKey),
|
||||||
}),
|
}),
|
||||||
|
|||||||
121
.codex/skills/wechatpay-basic-payment/SKILL.md
Normal file
121
.codex/skills/wechatpay-basic-payment/SKILL.md
Normal 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)、时间戳、随机串传入脚本。执行前需按分步确认协议征得同意。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 以下信息与技能能力无关,仅供查阅。
|
||||||
|
|
||||||
|
## 💬 社区与反馈
|
||||||
|
|
||||||
|
在使用过程中遇到问题、有改进建议,或者想和其他开发者交流接入经验,欢迎扫码添加企业微信进群,与官方团队和社区开发者一起讨论:
|
||||||
|
|
||||||
|

|
||||||
BIN
.codex/skills/wechatpay-basic-payment/assets/qrcode.jpg
Normal file
BIN
.codex/skills/wechatpay-basic-payment/assets/qrcode.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
@@ -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) | 原生 APP(iOS/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)
|
||||||
|
```
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
删除分账接收方
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
查询分账回退结果
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
申请分账账单
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
请求分账回退
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
添加分账接收方
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
解冻剩余资金
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|
||||||
|
查询剩余待分金额
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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,但并不保证它绝对可靠,商户需进一步调用后端查单确认支付结果。
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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。
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 sendReq:request];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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,商户自行处理展示 |
|
||||||
|
|
||||||
|
> **重要**:前端回调不保证绝对可靠,不可只依赖前端回调判断订单支付状态,订单状态需以后端查询订单和支付成功回调通知为准。
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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有前端回调,商户必须通过后端查单接口或支付成功回调通知来确认订单状态。
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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`
|
||||||
|
- 二维码仅支持微信"扫一扫"功能扫描,不支持长按识别或相册识别
|
||||||
|
- 商户需通过后端查单接口或支付成功回调通知来确认订单状态,不能仅依赖用户告知
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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){}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重要**:前端回调不保证绝对可靠,商户需通过后端查单接口或支付成功回调通知来确认订单状态。
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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或超时,微信支付会按策略重试通知
|
||||||
@@ -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 客户端,封装请求签名→发送→验签流程 |
|
||||||
@@ -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` 字段填写有误,需选择正确的门店所在省市区行政区划代码 |
|
||||||
@@ -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_id(2h) | prepay_id(2h) | h5_url(**5min**) | code_url(2h) | prepay_id(2h) |
|
||||||
|
| **调起方式** | 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)
|
||||||
|
```
|
||||||
@@ -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 支付场景**不支持** |
|
||||||
|
| 从业机构号可以开通点金计划吗? | **不支持**,需要从业机构下的渠道商去开通配置 |
|
||||||
@@ -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 修改 |
|
||||||
|
| 单个人/主体进件次数有限制吗? | 有限制,系统动态评估,具体以实际返回为准 |
|
||||||
|
| 商户已存在怎么办? | 根据提示先注销已有商户号,再重新进件 |
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
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 := &UnfreezeOrderRequest{
|
||||||
|
SubMchid: wxpay_utility.String("1900000109"),
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
SubMchid *string `json:"sub_mchid,omitempty"`
|
||||||
|
TransactionId *string `json:"transaction_id,omitempty"`
|
||||||
|
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||||
|
Description *string `json:"description,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 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"
|
||||||
|
)
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/partner/4015119446
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 查询结算账户修改申请状态
|
||||||
|
// 调用频率限制:100/秒。
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/partner/4013080340
|
||||||
|
config, err := wxpay_utility.CreateMchConfig(
|
||||||
|
"19xxxxxxxx", // 商户号
|
||||||
|
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号
|
||||||
|
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径
|
||||||
|
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID
|
||||||
|
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request := &GetApplicationRequest{
|
||||||
|
SubMchid: wxpay_utility.String("1511101111"),
|
||||||
|
ApplicationNo: wxpay_utility.String("102329389XXXX"),
|
||||||
|
AccountNumberRule: ACCOUNTNUMBERRULE_ACCOUNT_NUMBER_RULE_MASK_V1.Ptr(),
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := GetApplication(config, request)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("请求失败: %+v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("请求成功: %+v\n", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetApplication(config *wxpay_utility.MchConfig, request *GetApplicationRequest) (response *SubMerchantsGetApplicationResponse, err error) {
|
||||||
|
const (
|
||||||
|
host = "https://api.mch.weixin.qq.com"
|
||||||
|
method = "GET"
|
||||||
|
path = "/v3/apply4sub/sub_merchants/{sub_mchid}/application/{application_no}"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
reqUrl.Path = strings.Replace(reqUrl.Path, "{application_no}", url.PathEscape(*request.ApplicationNo), -1)
|
||||||
|
query := reqUrl.Query()
|
||||||
|
if request.AccountNumberRule != nil {
|
||||||
|
query.Add("account_number_rule", fmt.Sprintf("%v", *request.AccountNumberRule))
|
||||||
|
}
|
||||||
|
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 := &SubMerchantsGetApplicationResponse{}
|
||||||
|
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 GetApplicationRequest struct {
|
||||||
|
SubMchid *string `json:"sub_mchid,omitempty"`
|
||||||
|
ApplicationNo *string `json:"application_no,omitempty"`
|
||||||
|
AccountNumberRule *AccountNumberRule `json:"account_number_rule,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *GetApplicationRequest) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias GetApplicationRequest
|
||||||
|
a := &struct {
|
||||||
|
SubMchid *string `json:"sub_mchid,omitempty"`
|
||||||
|
ApplicationNo *string `json:"application_no,omitempty"`
|
||||||
|
AccountNumberRule *AccountNumberRule `json:"account_number_rule,omitempty"`
|
||||||
|
*Alias
|
||||||
|
}{
|
||||||
|
SubMchid: nil,
|
||||||
|
ApplicationNo: nil,
|
||||||
|
AccountNumberRule: nil,
|
||||||
|
Alias: (*Alias)(o),
|
||||||
|
}
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubMerchantsGetApplicationResponse struct {
|
||||||
|
AccountName *string `json:"account_name,omitempty"`
|
||||||
|
AccountType *BankAccountType `json:"account_type,omitempty"`
|
||||||
|
AccountBank *string `json:"account_bank,omitempty"`
|
||||||
|
BankName *string `json:"bank_name,omitempty"`
|
||||||
|
BankBranchId *string `json:"bank_branch_id,omitempty"`
|
||||||
|
AccountNumber *string `json:"account_number,omitempty"`
|
||||||
|
VerifyResult *AuditResult `json:"verify_result,omitempty"`
|
||||||
|
VerifyFailReason *string `json:"verify_fail_reason,omitempty"`
|
||||||
|
VerifyFinishTime *string `json:"verify_finish_time,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountNumberRule string
|
||||||
|
func (e AccountNumberRule) Ptr() *AccountNumberRule { return &e }
|
||||||
|
const (
|
||||||
|
ACCOUNTNUMBERRULE_ACCOUNT_NUMBER_RULE_MASK_V1 AccountNumberRule = "ACCOUNT_NUMBER_RULE_MASK_V1"
|
||||||
|
ACCOUNTNUMBERRULE_ACCOUNT_NUMBER_RULE_MASK_V2 AccountNumberRule = "ACCOUNT_NUMBER_RULE_MASK_V2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BankAccountType string
|
||||||
|
func (e BankAccountType) Ptr() *BankAccountType { return &e }
|
||||||
|
const (
|
||||||
|
BANKACCOUNTTYPE_ACCOUNT_TYPE_BUSINESS BankAccountType = "ACCOUNT_TYPE_BUSINESS"
|
||||||
|
BANKACCOUNTTYPE_ACCOUNT_TYPE_PRIVATE BankAccountType = "ACCOUNT_TYPE_PRIVATE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuditResult string
|
||||||
|
func (e AuditResult) Ptr() *AuditResult { return &e }
|
||||||
|
const (
|
||||||
|
AUDITRESULT_AUDIT_SUCCESS AuditResult = "AUDIT_SUCCESS"
|
||||||
|
AUDITRESULT_AUDITING AuditResult = "AUDITING"
|
||||||
|
AUDITRESULT_AUDIT_FAIL AuditResult = "AUDIT_FAIL"
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user