Compare commits
9 Commits
238d93cdac
...
1d08953fce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d08953fce | ||
|
|
db85c0ab8a | ||
|
|
ca567ce310 | ||
|
|
a6825a62bb | ||
|
|
9fd12b2a09 | ||
|
|
10f71234e4 | ||
|
|
968a4a453f | ||
|
|
028074cf5b | ||
|
|
087c85c738 |
@@ -106,6 +106,13 @@
|
||||
- 企业微信:`weixin-java-cp`
|
||||
- 微信视频号/微信小店:`weixin-java-channel`
|
||||
|
||||
**注意**:
|
||||
- **移动应用开发**:如果你的移动应用(iOS/Android App)需要接入微信登录、分享等功能:
|
||||
- 微信登录(网页授权):使用 `weixin-java-open` 模块,在服务端处理 OAuth 授权
|
||||
- 微信支付:使用 `weixin-java-pay` 模块
|
||||
- 客户端集成:需使用微信官方提供的移动端SDK(iOS/Android),本项目为服务端SDK
|
||||
- **微信开放平台**(`weixin-java-open`)主要用于第三方平台,代公众号或小程序进行开发和管理
|
||||
|
||||
|
||||
---------------------------------
|
||||
### 版本说明
|
||||
|
||||
@@ -28,6 +28,7 @@ public class WxOpenServiceAutoConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) {
|
||||
return new WxOpenMessageRouter(wxOpenService);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,23 @@ public interface WxCpGroupRobotService {
|
||||
*/
|
||||
void sendMarkdown(String webhookUrl, String content) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 发送markdown_v2类型的消息
|
||||
*
|
||||
* @param content markdown内容,最长不超过4096个字节,必须是utf8编码
|
||||
* @throws WxErrorException 异常
|
||||
*/
|
||||
void sendMarkdownV2(String content) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 发送markdown_v2类型的消息
|
||||
*
|
||||
* @param webhookUrl webhook地址
|
||||
* @param content markdown内容,最长不超过4096个字节,必须是utf8编码
|
||||
* @throws WxErrorException 异常
|
||||
*/
|
||||
void sendMarkdownV2(String webhookUrl, String content) throws WxErrorException;
|
||||
|
||||
/**
|
||||
* 发送image类型的消息
|
||||
*
|
||||
|
||||
@@ -42,6 +42,11 @@ public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService {
|
||||
this.sendMarkdown(this.getWebhookUrl(), content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMarkdownV2(String content) throws WxErrorException {
|
||||
this.sendMarkdownV2(this.getWebhookUrl(), content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendImage(String base64, String md5) throws WxErrorException {
|
||||
this.sendImage(this.getWebhookUrl(), base64, md5);
|
||||
@@ -70,6 +75,14 @@ public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService {
|
||||
.toJson());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMarkdownV2(String webhookUrl, String content) throws WxErrorException {
|
||||
this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage()
|
||||
.setMsgType(GroupRobotMsgType.MARKDOWN_V2)
|
||||
.setContent(content)
|
||||
.toJson());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendImage(String webhookUrl, String base64, String md5) throws WxErrorException {
|
||||
this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage()
|
||||
|
||||
@@ -252,6 +252,12 @@ public class WxCpGroupRobotMessage implements Serializable {
|
||||
messageJson.add("markdown", text);
|
||||
break;
|
||||
}
|
||||
case MARKDOWN_V2: {
|
||||
JsonObject text = new JsonObject();
|
||||
text.addProperty("content", this.getContent());
|
||||
messageJson.add("markdown_v2", text);
|
||||
break;
|
||||
}
|
||||
case IMAGE: {
|
||||
JsonObject text = new JsonObject();
|
||||
text.addProperty("base64", this.getBase64());
|
||||
|
||||
@@ -6,6 +6,8 @@ import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -126,6 +128,74 @@ public class ContentValue implements Serializable {
|
||||
@SerializedName("tips_content")
|
||||
private List<TipsContent> tipsContent;
|
||||
|
||||
/**
|
||||
* Creates a simple Tips control with mixed plain text and clickable links.
|
||||
*
|
||||
* @param lang the language code (e.g., "zh_CN")
|
||||
* @param textAndLinks array of objects where strings become plain text and Link objects become clickable links
|
||||
* @return NewTips instance with the specified content
|
||||
*/
|
||||
public static NewTips of(String lang, Object... textAndLinks) {
|
||||
NewTips tips = new NewTips();
|
||||
TipsContent content = new TipsContent();
|
||||
TipsContent.Text text = new TipsContent.Text();
|
||||
|
||||
List<TipsContent.SubText> subTexts = new ArrayList<>();
|
||||
|
||||
for (Object item : textAndLinks) {
|
||||
TipsContent.SubText subText = new TipsContent.SubText();
|
||||
TipsContent.SubText.Content subContent = new TipsContent.SubText.Content();
|
||||
|
||||
if (item instanceof String) {
|
||||
// Plain text
|
||||
TipsContent.SubText.Content.PlainText plainText = new TipsContent.SubText.Content.PlainText();
|
||||
plainText.setContent((String) item);
|
||||
subContent.setPlainText(plainText);
|
||||
subText.setType(1);
|
||||
} else if (item instanceof TipsContent.SubText.Content.Link) {
|
||||
// Link
|
||||
subContent.setLink((TipsContent.SubText.Content.Link) item);
|
||||
subText.setType(2);
|
||||
}
|
||||
|
||||
subText.setContent(subContent);
|
||||
subTexts.add(subText);
|
||||
}
|
||||
|
||||
text.setSubText(subTexts);
|
||||
content.setText(text);
|
||||
content.setLang(lang);
|
||||
tips.setTipsContent(Arrays.asList(content));
|
||||
|
||||
return tips;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple Tips control with only plain text.
|
||||
*
|
||||
* @param lang the language code (e.g., "zh_CN")
|
||||
* @param textContent the plain text content
|
||||
* @return NewTips instance with plain text content
|
||||
*/
|
||||
public static NewTips ofText(String lang, String textContent) {
|
||||
return of(lang, textContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Tips control with a single clickable link.
|
||||
*
|
||||
* @param lang the language code (e.g., "zh_CN")
|
||||
* @param linkTitle the display text for the link
|
||||
* @param linkUrl the URL to link to
|
||||
* @return NewTips instance with a single clickable link
|
||||
*/
|
||||
public static NewTips ofLink(String lang, String linkTitle, String linkUrl) {
|
||||
TipsContent.SubText.Content.Link link = new TipsContent.SubText.Content.Link();
|
||||
link.setTitle(linkTitle);
|
||||
link.setUrl(linkUrl);
|
||||
return of(lang, link);
|
||||
}
|
||||
|
||||
/**
|
||||
* The type tips_content.
|
||||
*/
|
||||
|
||||
@@ -630,6 +630,11 @@ public class WxCpConsts {
|
||||
*/
|
||||
public static final String MARKDOWN = "markdown";
|
||||
|
||||
/**
|
||||
* markdown_v2消息.
|
||||
*/
|
||||
public static final String MARKDOWN_V2 = "markdown_v2";
|
||||
|
||||
/**
|
||||
* 图文消息(点击跳转到外链).
|
||||
*/
|
||||
|
||||
@@ -281,7 +281,12 @@ public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSeri
|
||||
}
|
||||
addProperty(o, MAIN_DEPARTMENT, user.getMainDepartment());
|
||||
|
||||
addArrayProperty(o, DIRECT_LEADER, user.getDirectLeader());
|
||||
// Special handling for directLeader: include empty arrays to support WeChat Work API reset functionality
|
||||
if (user.getDirectLeader() != null) {
|
||||
JsonArray directLeaderArray = new JsonArray();
|
||||
Arrays.stream(user.getDirectLeader()).forEach(directLeaderArray::add);
|
||||
o.add(DIRECT_LEADER, directLeaderArray);
|
||||
}
|
||||
|
||||
if (!user.getExtAttrs().isEmpty()) {
|
||||
JsonArray attrsJsonArray = new JsonArray();
|
||||
|
||||
@@ -64,6 +64,51 @@ public class WxCpGroupRobotServiceImplTest {
|
||||
robotService.sendMarkdown(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test send mark down v2.
|
||||
*
|
||||
* @throws WxErrorException the wx error exception
|
||||
*/
|
||||
@Test
|
||||
public void testSendMarkDownV2() throws WxErrorException {
|
||||
String content = "# 一、标题\n" +
|
||||
"## 二级标题\n" +
|
||||
"### 三级标题\n" +
|
||||
"# 二、字体\n" +
|
||||
"*斜体*\n" +
|
||||
"\n" +
|
||||
"**加粗**\n" +
|
||||
"# 三、列表 \n" +
|
||||
"- 无序列表 1 \n" +
|
||||
"- 无序列表 2\n" +
|
||||
" - 无序列表 2.1\n" +
|
||||
" - 无序列表 2.2\n" +
|
||||
"1. 有序列表 1\n" +
|
||||
"2. 有序列表 2\n" +
|
||||
"# 四、引用\n" +
|
||||
"> 一级引用\n" +
|
||||
">>二级引用\n" +
|
||||
">>>三级引用\n" +
|
||||
"# 五、链接\n" +
|
||||
"[这是一个链接](https://work.weixin.qq.com/api/doc)\n" +
|
||||
"\n" +
|
||||
"# 六、分割线\n" +
|
||||
"\n" +
|
||||
"---\n" +
|
||||
"# 七、代码\n" +
|
||||
"`这是行内代码`\n" +
|
||||
"```\n" +
|
||||
"这是独立代码块\n" +
|
||||
"```\n" +
|
||||
"\n" +
|
||||
"# 八、表格\n" +
|
||||
"| 姓名 | 文化衫尺寸 | 收货地址 |\n" +
|
||||
"| :----- | :----: | -------: |\n" +
|
||||
"| 张三 | S | 广州 |\n" +
|
||||
"| 李四 | L | 深圳 |";
|
||||
robotService.sendMarkdownV2(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test send image.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package me.chanjar.weixin.cp.bean.oa.applydata;
|
||||
|
||||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
public class ContentValueTipsTest {
|
||||
|
||||
@Test
|
||||
public void testTipsWithLinksManualCreation() {
|
||||
System.out.println("Testing ContentValue.NewTips structure with Link (manual creation):");
|
||||
|
||||
// Create a Tips structure with both plain text and link
|
||||
ContentValue.NewTips tips = new ContentValue.NewTips();
|
||||
ContentValue.NewTips.TipsContent tipsContent = new ContentValue.NewTips.TipsContent();
|
||||
ContentValue.NewTips.TipsContent.Text text = new ContentValue.NewTips.TipsContent.Text();
|
||||
|
||||
// Create plain text subtext
|
||||
ContentValue.NewTips.TipsContent.SubText plainSubText = new ContentValue.NewTips.TipsContent.SubText();
|
||||
ContentValue.NewTips.TipsContent.SubText.Content plainContent = new ContentValue.NewTips.TipsContent.SubText.Content();
|
||||
ContentValue.NewTips.TipsContent.SubText.Content.PlainText plainTextContent =
|
||||
new ContentValue.NewTips.TipsContent.SubText.Content.PlainText();
|
||||
plainTextContent.setContent("This is plain text. For more info, ");
|
||||
plainContent.setPlainText(plainTextContent);
|
||||
plainSubText.setType(1); // Type 1 for plain text
|
||||
plainSubText.setContent(plainContent);
|
||||
|
||||
// Create link subtext
|
||||
ContentValue.NewTips.TipsContent.SubText linkSubText = new ContentValue.NewTips.TipsContent.SubText();
|
||||
ContentValue.NewTips.TipsContent.SubText.Content linkContent = new ContentValue.NewTips.TipsContent.SubText.Content();
|
||||
ContentValue.NewTips.TipsContent.SubText.Content.Link link =
|
||||
new ContentValue.NewTips.TipsContent.SubText.Content.Link();
|
||||
link.setTitle("click here");
|
||||
link.setUrl("https://work.weixin.qq.com");
|
||||
linkContent.setLink(link);
|
||||
linkSubText.setType(2); // Type 2 for link
|
||||
linkSubText.setContent(linkContent);
|
||||
|
||||
text.setSubText(Arrays.asList(plainSubText, linkSubText));
|
||||
tipsContent.setText(text);
|
||||
tipsContent.setLang("zh_CN");
|
||||
|
||||
tips.setTipsContent(Arrays.asList(tipsContent));
|
||||
|
||||
// Convert to JSON
|
||||
String json = WxCpGsonBuilder.create().toJson(tips);
|
||||
System.out.println("Generated JSON:");
|
||||
System.out.println(json);
|
||||
|
||||
// Try to parse it back
|
||||
validateTipsStructure(tips, json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTipsWithConvenienceMethods() {
|
||||
System.out.println("Testing ContentValue.NewTips with convenience methods:");
|
||||
|
||||
// Test 1: Simple plain text
|
||||
ContentValue.NewTips textOnly = ContentValue.NewTips.ofText("zh_CN", "This is a simple text tip.");
|
||||
String textJson = WxCpGsonBuilder.create().toJson(textOnly);
|
||||
System.out.println("Text-only JSON: " + textJson);
|
||||
validateTipsStructure(textOnly, textJson);
|
||||
|
||||
// Test 2: Single link
|
||||
ContentValue.NewTips linkOnly = ContentValue.NewTips.ofLink("zh_CN", "Visit WeChat Work", "https://work.weixin.qq.com");
|
||||
String linkJson = WxCpGsonBuilder.create().toJson(linkOnly);
|
||||
System.out.println("Link-only JSON: " + linkJson);
|
||||
validateTipsStructure(linkOnly, linkJson);
|
||||
|
||||
// Test 3: Mixed content using convenience method
|
||||
ContentValue.NewTips.TipsContent.SubText.Content.Link link =
|
||||
new ContentValue.NewTips.TipsContent.SubText.Content.Link();
|
||||
link.setTitle("click here");
|
||||
link.setUrl("https://work.weixin.qq.com");
|
||||
|
||||
ContentValue.NewTips mixed = ContentValue.NewTips.of("zh_CN",
|
||||
"For more information, ", link, " or contact support.");
|
||||
String mixedJson = WxCpGsonBuilder.create().toJson(mixed);
|
||||
System.out.println("Mixed content JSON: " + mixedJson);
|
||||
validateTipsStructure(mixed, mixedJson);
|
||||
|
||||
System.out.println("All convenience method tests passed!");
|
||||
}
|
||||
|
||||
private void validateTipsStructure(ContentValue.NewTips tips, String json) {
|
||||
try {
|
||||
ContentValue.NewTips parsedTips = WxCpGsonBuilder.create().fromJson(json, ContentValue.NewTips.class);
|
||||
assertNotNull(parsedTips);
|
||||
assertNotNull(parsedTips.getTipsContent());
|
||||
assertFalse(parsedTips.getTipsContent().isEmpty());
|
||||
|
||||
ContentValue.NewTips.TipsContent.Text parsedText = parsedTips.getTipsContent().get(0).getText();
|
||||
assertNotNull(parsedText);
|
||||
assertNotNull(parsedText.getSubText());
|
||||
assertTrue(parsedText.getSubText().size() > 0);
|
||||
|
||||
// Verify structure based on content
|
||||
for (ContentValue.NewTips.TipsContent.SubText subText : parsedText.getSubText()) {
|
||||
assertNotNull(subText.getType());
|
||||
assertNotNull(subText.getContent());
|
||||
|
||||
if (subText.getType() == 1) {
|
||||
// Plain text
|
||||
assertNotNull(subText.getContent().getPlainText());
|
||||
assertNotNull(subText.getContent().getPlainText().getContent());
|
||||
} else if (subText.getType() == 2) {
|
||||
// Link
|
||||
assertNotNull(subText.getContent().getLink());
|
||||
assertNotNull(subText.getContent().getLink().getTitle());
|
||||
assertNotNull(subText.getContent().getLink().getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("✓ JSON parsing and validation successful");
|
||||
} catch (Exception e) {
|
||||
System.out.println("✗ Error parsing: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
fail("Failed to parse JSON: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package me.chanjar.weixin.cp.bean.oa.applydata;
|
||||
|
||||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
|
||||
|
||||
/**
|
||||
* Usage examples for ContentValue.NewTips with clickable link support.
|
||||
*
|
||||
* This example demonstrates how to create Tips controls that can render URLs as clickable links,
|
||||
* addressing the issue where "Tips控件无法将url渲染为可点击的链接".
|
||||
*
|
||||
* @author WxJava Community
|
||||
*/
|
||||
public class TipsUsageExample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
demonstrateBasicUsage();
|
||||
demonstrateAdvancedUsage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic usage examples for creating Tips with clickable links.
|
||||
*/
|
||||
public static void demonstrateBasicUsage() {
|
||||
System.out.println("=== Basic Tips Usage Examples ===\n");
|
||||
|
||||
// Example 1: Simple plain text tip
|
||||
ContentValue.NewTips textTip = ContentValue.NewTips.ofText("zh_CN",
|
||||
"这是一个简单的文本提示。");
|
||||
System.out.println("1. Plain text tip JSON:");
|
||||
System.out.println(WxCpGsonBuilder.create().toJson(textTip));
|
||||
System.out.println();
|
||||
|
||||
// Example 2: Simple clickable link tip
|
||||
ContentValue.NewTips linkTip = ContentValue.NewTips.ofLink("zh_CN",
|
||||
"访问企业微信官网", "https://work.weixin.qq.com");
|
||||
System.out.println("2. Single link tip JSON:");
|
||||
System.out.println(WxCpGsonBuilder.create().toJson(linkTip));
|
||||
System.out.println();
|
||||
|
||||
// Example 3: Mixed content - text with clickable link
|
||||
ContentValue.NewTips.TipsContent.SubText.Content.Link helpLink =
|
||||
new ContentValue.NewTips.TipsContent.SubText.Content.Link();
|
||||
helpLink.setTitle("点击查看详情");
|
||||
helpLink.setUrl("https://work.weixin.qq.com/help");
|
||||
|
||||
ContentValue.NewTips mixedTip = ContentValue.NewTips.of("zh_CN",
|
||||
"如需了解更多信息,请", helpLink, "。");
|
||||
System.out.println("3. Mixed content tip JSON:");
|
||||
System.out.println(WxCpGsonBuilder.create().toJson(mixedTip));
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced usage examples showing complex Tips with multiple links and text.
|
||||
*/
|
||||
public static void demonstrateAdvancedUsage() {
|
||||
System.out.println("=== Advanced Tips Usage Examples ===\n");
|
||||
|
||||
// Example 4: Complex tip with multiple links
|
||||
ContentValue.NewTips.TipsContent.SubText.Content.Link docsLink =
|
||||
new ContentValue.NewTips.TipsContent.SubText.Content.Link();
|
||||
docsLink.setTitle("开发文档");
|
||||
docsLink.setUrl("https://developer.work.weixin.qq.com");
|
||||
|
||||
ContentValue.NewTips.TipsContent.SubText.Content.Link supportLink =
|
||||
new ContentValue.NewTips.TipsContent.SubText.Content.Link();
|
||||
supportLink.setTitle("技术支持");
|
||||
supportLink.setUrl("https://work.weixin.qq.com/contact");
|
||||
|
||||
ContentValue.NewTips complexTip = ContentValue.NewTips.of("zh_CN",
|
||||
"审批流程说明:\n1. 提交申请后系统将自动处理\n2. 如有疑问请查看",
|
||||
docsLink,
|
||||
"或联系",
|
||||
supportLink);
|
||||
System.out.println("4. Complex tip with multiple links JSON:");
|
||||
System.out.println(WxCpGsonBuilder.create().toJson(complexTip));
|
||||
System.out.println();
|
||||
|
||||
// Demonstrate that the structure supports proper type differentiation
|
||||
System.out.println("=== Type Verification ===");
|
||||
ContentValue.NewTips parsed = WxCpGsonBuilder.create().fromJson(
|
||||
WxCpGsonBuilder.create().toJson(complexTip), ContentValue.NewTips.class);
|
||||
|
||||
parsed.getTipsContent().get(0).getText().getSubText().forEach(subText -> {
|
||||
if (subText.getType() == 1) {
|
||||
System.out.println("Plain text: \"" + subText.getContent().getPlainText().getContent() + "\"");
|
||||
} else if (subText.getType() == 2) {
|
||||
System.out.println("Link: \"" + subText.getContent().getLink().getTitle() +
|
||||
"\" -> " + subText.getContent().getLink().getUrl());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -180,4 +180,31 @@ public class WxCpUserGsonAdapterTest {
|
||||
"{\"type\":2,\"name\":\"测试app\"," +
|
||||
"\"miniprogram\":{\"appid\":\"wx8bd80126147df384\",\"pagepath\":\"/index\",\"title\":\"my miniprogram\"}}]}}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test directLeader empty array serialization.
|
||||
* This test verifies that empty directLeader arrays are included in JSON as "direct_leader":[]
|
||||
* instead of being omitted, which is required for WeChat Work API to reset user direct leaders.
|
||||
*/
|
||||
@Test
|
||||
public void testDirectLeaderEmptyArraySerialization() {
|
||||
WxCpUser user = new WxCpUser();
|
||||
user.setUserId("testuser");
|
||||
user.setName("Test User");
|
||||
|
||||
// Test with empty array - should be serialized as "direct_leader":[]
|
||||
user.setDirectLeader(new String[]{});
|
||||
String json = user.toJson();
|
||||
assertThat(json).contains("\"direct_leader\":[]");
|
||||
|
||||
// Test with null - should not include direct_leader field
|
||||
user.setDirectLeader(null);
|
||||
json = user.toJson();
|
||||
assertThat(json).doesNotContain("direct_leader");
|
||||
|
||||
// Test with non-empty array - should be serialized normally
|
||||
user.setDirectLeader(new String[]{"leader1", "leader2"});
|
||||
json = user.toJson();
|
||||
assertThat(json).contains("\"direct_leader\":[\"leader1\",\"leader2\"]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,38 @@
|
||||
# 微信开放平台模块 (weixin-java-open)
|
||||
|
||||
## 模块说明
|
||||
|
||||
本模块主要用于**微信第三方平台**的开发,适用于以下场景:
|
||||
|
||||
### 适用场景
|
||||
1. **第三方平台开发**:作为第三方平台,代替多个公众号或小程序进行管理和开发
|
||||
2. **代公众号实现业务**:通过授权代替公众号进行消息管理、素材管理等操作
|
||||
3. **代小程序实现业务**:通过授权代替小程序进行代码管理、基本信息设置等操作
|
||||
|
||||
### 移动应用开发说明
|
||||
|
||||
**如果您要开发移动应用(iOS/Android App)并接入微信功能,请注意:**
|
||||
|
||||
- **微信登录**:
|
||||
- 移动应用的微信登录(网页授权)需要在**微信开放平台**(open.weixin.qq.com)创建移动应用
|
||||
- 服务端处理 OAuth 授权时使用本模块 `weixin-java-open`
|
||||
- 移动端需集成微信官方SDK(iOS/Android),本项目仅提供服务端SDK
|
||||
|
||||
- **微信支付**:
|
||||
- 使用 `weixin-java-pay` 模块,参考 [微信支付文档](../weixin-java-pay/)
|
||||
- 移动应用支付使用 APP 支付类型(TradeType.APP)
|
||||
|
||||
- **微信分享**:
|
||||
- 需集成微信官方移动端SDK,本项目不涉及客户端功能
|
||||
|
||||
**参考资料**:
|
||||
- [微信开放平台官方文档](https://open.weixin.qq.com/)
|
||||
- [移动应用接入指南](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html)
|
||||
|
||||
---
|
||||
|
||||
## 代码示例
|
||||
|
||||
消息机制未实现,下面为通知回调中设置的代码部分
|
||||
|
||||
以下代码可通过腾讯全网发布测试用例
|
||||
|
||||
120
weixin-java-pay/OVERSEAS_PAY.md
Normal file
120
weixin-java-pay/OVERSEAS_PAY.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 境外微信支付(Overseas WeChat Pay)支持
|
||||
|
||||
本次更新添加了境外微信支付的支持,解决了 [Issue #3618](https://github.com/binarywang/WxJava/issues/3618) 中提到的问题。
|
||||
|
||||
## 问题背景
|
||||
|
||||
境外微信支付需要使用新的API接口地址和额外的参数:
|
||||
- 使用不同的基础URL: `https://apihk.mch.weixin.qq.com`
|
||||
- 需要额外的参数: `trade_type` 和 `merchant_category_code`
|
||||
- 使用不同的API端点: `/global/v3/transactions/*`
|
||||
|
||||
## 新增功能
|
||||
|
||||
### 1. GlobalTradeTypeEnum
|
||||
新的枚举类,定义了境外支付的交易类型和对应的API端点:
|
||||
- `APP`: `/global/v3/transactions/app`
|
||||
- `JSAPI`: `/global/v3/transactions/jsapi`
|
||||
- `NATIVE`: `/global/v3/transactions/native`
|
||||
- `H5`: `/global/v3/transactions/h5`
|
||||
|
||||
### 2. WxPayUnifiedOrderV3GlobalRequest
|
||||
扩展的请求类,包含境外支付必需的额外字段:
|
||||
- `trade_type`: 交易类型 (JSAPI, APP, NATIVE, H5)
|
||||
- `merchant_category_code`: 商户类目代码(境外商户必填)
|
||||
|
||||
### 3. 新的服务方法
|
||||
- `createOrderV3Global()`: 创建境外支付订单
|
||||
- `unifiedOrderV3Global()`: 境外统一下单接口
|
||||
|
||||
## 使用示例
|
||||
|
||||
### JSAPI支付示例
|
||||
```java
|
||||
// 创建境外支付请求
|
||||
WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
|
||||
request.setOutTradeNo(RandomUtils.getRandomStr());
|
||||
request.setDescription("境外商品购买");
|
||||
request.setNotifyUrl("https://your-domain.com/notify");
|
||||
|
||||
// 设置金额
|
||||
WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
|
||||
amount.setCurrency(WxPayConstants.CurrencyType.CNY);
|
||||
amount.setTotal(100); // 1元,单位为分
|
||||
request.setAmount(amount);
|
||||
|
||||
// 设置支付者
|
||||
WxPayUnifiedOrderV3GlobalRequest.Payer payer = new WxPayUnifiedOrderV3GlobalRequest.Payer();
|
||||
payer.setOpenid("用户的openid");
|
||||
request.setPayer(payer);
|
||||
|
||||
// 设置境外支付必需的参数
|
||||
request.setTradeType("JSAPI");
|
||||
request.setMerchantCategoryCode("5812"); // 商户类目代码
|
||||
|
||||
// 调用境外支付接口
|
||||
WxPayUnifiedOrderV3Result.JsapiResult result = payService.createOrderV3Global(
|
||||
GlobalTradeTypeEnum.JSAPI,
|
||||
request
|
||||
);
|
||||
```
|
||||
|
||||
### APP支付示例
|
||||
```java
|
||||
WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
|
||||
// ... 设置基础信息 ...
|
||||
|
||||
request.setTradeType("APP");
|
||||
request.setMerchantCategoryCode("5812");
|
||||
request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer()); // APP支付不需要openid
|
||||
|
||||
WxPayUnifiedOrderV3Result.AppResult result = payService.createOrderV3Global(
|
||||
GlobalTradeTypeEnum.APP,
|
||||
request
|
||||
);
|
||||
```
|
||||
|
||||
### NATIVE支付示例
|
||||
```java
|
||||
WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
|
||||
// ... 设置基础信息 ...
|
||||
|
||||
request.setTradeType("NATIVE");
|
||||
request.setMerchantCategoryCode("5812");
|
||||
request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer());
|
||||
|
||||
String codeUrl = payService.createOrderV3Global(
|
||||
GlobalTradeTypeEnum.NATIVE,
|
||||
request
|
||||
);
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
境外支付使用相同的 `WxPayConfig` 配置,无需特殊设置:
|
||||
|
||||
```java
|
||||
WxPayConfig config = new WxPayConfig();
|
||||
config.setAppId("你的AppId");
|
||||
config.setMchId("你的境外商户号");
|
||||
config.setMchKey("你的商户密钥");
|
||||
config.setNotifyUrl("https://your-domain.com/notify");
|
||||
|
||||
// V3相关配置
|
||||
config.setPrivateKeyPath("你的私钥文件路径");
|
||||
config.setCertSerialNo("你的商户证书序列号");
|
||||
config.setApiV3Key("你的APIv3密钥");
|
||||
```
|
||||
|
||||
**注意**: 境外支付会自动使用 `https://apihk.mch.weixin.qq.com` 作为基础URL,无需手动设置。
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 完全向后兼容,不影响现有的国内支付功能
|
||||
- 使用相同的配置类和结果类
|
||||
- 遵循现有的代码风格和架构模式
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [境外微信支付文档](https://pay.weixin.qq.com/doc/global/v3/zh/4013014223)
|
||||
- [原始Issue #3618](https://github.com/binarywang/WxJava/issues/3618)
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.github.binarywang.wxpay.bean.request;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 境外微信支付统一下单请求参数对象.
|
||||
* 参考文档:https://pay.weixin.qq.com/doc/global/v3/zh/4013014223
|
||||
* </pre>
|
||||
*
|
||||
* @author Binary Wang
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class WxPayUnifiedOrderV3GlobalRequest extends WxPayUnifiedOrderV3Request implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 字段名:交易类型
|
||||
* 变量名:trade_type
|
||||
* 是否必填:是
|
||||
* 类型:string[1,16]
|
||||
* 描述:
|
||||
* 交易类型,取值如下:
|
||||
* JSAPI--JSAPI支付
|
||||
* NATIVE--Native支付
|
||||
* APP--APP支付
|
||||
* H5--H5支付
|
||||
* 示例值:JSAPI
|
||||
* </pre>
|
||||
*/
|
||||
@SerializedName(value = "trade_type")
|
||||
private String tradeType;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 字段名:商户类目
|
||||
* 变量名:merchant_category_code
|
||||
* 是否必填:是
|
||||
* 类型:string[1,32]
|
||||
* 描述:
|
||||
* 商户类目,境外商户必填
|
||||
* 示例值:5812
|
||||
* </pre>
|
||||
*/
|
||||
@SerializedName(value = "merchant_category_code")
|
||||
private String merchantCategoryCode;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.github.binarywang.wxpay.bean.result.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 境外微信支付方式
|
||||
* Overseas WeChat Pay trade types with global endpoints
|
||||
*
|
||||
* @author Binary Wang
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum GlobalTradeTypeEnum {
|
||||
/**
|
||||
* APP
|
||||
*/
|
||||
APP("/global/v3/transactions/app"),
|
||||
/**
|
||||
* JSAPI 或 小程序
|
||||
*/
|
||||
JSAPI("/global/v3/transactions/jsapi"),
|
||||
/**
|
||||
* NATIVE
|
||||
*/
|
||||
NATIVE("/global/v3/transactions/native"),
|
||||
/**
|
||||
* H5
|
||||
*/
|
||||
H5("/global/v3/transactions/h5");
|
||||
|
||||
/**
|
||||
* 境外下单url
|
||||
*/
|
||||
private final String url;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.github.binarywang.wxpay.bean.notify.*;
|
||||
import com.github.binarywang.wxpay.bean.request.*;
|
||||
import com.github.binarywang.wxpay.bean.result.*;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
@@ -640,6 +641,17 @@ public interface WxPayService {
|
||||
*/
|
||||
<T> T createPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException;
|
||||
|
||||
/**
|
||||
* 境外微信支付调用统一下单接口,并组装生成支付所需参数对象.
|
||||
*
|
||||
* @param <T> 请使用{@link WxPayUnifiedOrderV3Result}里的内部类或字段
|
||||
* @param tradeType the global trade type
|
||||
* @param request 境外统一下单请求参数
|
||||
* @return 返回 {@link WxPayUnifiedOrderV3Result}里的内部类或字段
|
||||
* @throws WxPayException the wx pay exception
|
||||
*/
|
||||
<T> T createOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException;
|
||||
|
||||
/**
|
||||
* 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
|
||||
*
|
||||
@@ -660,6 +672,16 @@ public interface WxPayService {
|
||||
*/
|
||||
WxPayUnifiedOrderV3Result unifiedOrderV3(TradeTypeEnum tradeType, WxPayUnifiedOrderV3Request request) throws WxPayException;
|
||||
|
||||
/**
|
||||
* 境外微信支付在发起支付前,需要调用统一下单接口,获取"预支付交易会话标识"
|
||||
*
|
||||
* @param tradeType the global trade type
|
||||
* @param request 境外请求对象,注意一些参数如appid、mchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置中已经设置)
|
||||
* @return the wx pay unified order result
|
||||
* @throws WxPayException the wx pay exception
|
||||
*/
|
||||
WxPayUnifiedOrderV3Result unifiedOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 合单支付API(APP支付、JSAPI支付、H5支付、NATIVE支付).
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.*;
|
||||
import com.github.binarywang.wxpay.bean.result.*;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfigHolder;
|
||||
@@ -746,6 +747,14 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
|
||||
return result.getPayInfo(tradeType, appId, request.getSubMchId(), this.getConfig().getPrivateKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T createOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException {
|
||||
WxPayUnifiedOrderV3Result result = this.unifiedOrderV3Global(tradeType, request);
|
||||
// Convert GlobalTradeTypeEnum to TradeTypeEnum for getPayInfo method
|
||||
TradeTypeEnum domesticTradeType = TradeTypeEnum.valueOf(tradeType.name());
|
||||
return result.getPayInfo(domesticTradeType, request.getAppid(), request.getMchid(), this.getConfig().getPrivateKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxPayUnifiedOrderV3Result unifiedPartnerOrderV3(TradeTypeEnum tradeType, WxPayPartnerUnifiedOrderV3Request request) throws WxPayException {
|
||||
if (StringUtils.isBlank(request.getSpAppid())) {
|
||||
@@ -790,6 +799,28 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
|
||||
return GSON.fromJson(response, WxPayUnifiedOrderV3Result.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxPayUnifiedOrderV3Result unifiedOrderV3Global(GlobalTradeTypeEnum tradeType, WxPayUnifiedOrderV3GlobalRequest request) throws WxPayException {
|
||||
if (StringUtils.isBlank(request.getAppid())) {
|
||||
request.setAppid(this.getConfig().getAppId());
|
||||
}
|
||||
if (StringUtils.isBlank(request.getMchid())) {
|
||||
request.setMchid(this.getConfig().getMchId());
|
||||
}
|
||||
if (StringUtils.isBlank(request.getNotifyUrl())) {
|
||||
request.setNotifyUrl(this.getConfig().getNotifyUrl());
|
||||
}
|
||||
if (StringUtils.isBlank(request.getTradeType())) {
|
||||
request.setTradeType(tradeType.name());
|
||||
}
|
||||
|
||||
// Use global WeChat Pay base URL for overseas payments
|
||||
String globalBaseUrl = "https://apihk.mch.weixin.qq.com";
|
||||
String url = globalBaseUrl + tradeType.getUrl();
|
||||
String response = this.postV3WithWechatpaySerial(url, GSON.toJson(request));
|
||||
return GSON.fromJson(response, WxPayUnifiedOrderV3Result.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CombineTransactionsResult combine(TradeTypeEnum tradeType, CombineTransactionsRequest request) throws WxPayException {
|
||||
if (StringUtils.isBlank(request.getCombineAppid())) {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.github.binarywang.wxpay.service.impl;
|
||||
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3GlobalRequest;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import me.chanjar.weixin.common.util.RandomUtils;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
/**
|
||||
* 境外微信支付测试类
|
||||
*
|
||||
* @author Binary Wang
|
||||
*/
|
||||
public class BaseWxPayServiceGlobalImplTest {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
|
||||
@Test
|
||||
public void testWxPayUnifiedOrderV3GlobalRequest() {
|
||||
// Test that the new request class has the required fields
|
||||
WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
|
||||
|
||||
// Set basic order information
|
||||
String outTradeNo = RandomUtils.getRandomStr();
|
||||
request.setOutTradeNo(outTradeNo);
|
||||
request.setDescription("Test overseas payment");
|
||||
request.setNotifyUrl("https://api.example.com/notify");
|
||||
|
||||
// Set amount
|
||||
WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
|
||||
amount.setCurrency(WxPayConstants.CurrencyType.CNY);
|
||||
amount.setTotal(100); // 1 yuan in cents
|
||||
request.setAmount(amount);
|
||||
|
||||
// Set payer
|
||||
WxPayUnifiedOrderV3GlobalRequest.Payer payer = new WxPayUnifiedOrderV3GlobalRequest.Payer();
|
||||
payer.setOpenid("test_openid");
|
||||
request.setPayer(payer);
|
||||
|
||||
// Set the new required fields for global payments
|
||||
request.setTradeType("JSAPI");
|
||||
request.setMerchantCategoryCode("5812"); // Example category code
|
||||
|
||||
// Assert that all fields are properly set
|
||||
assertNotNull(request.getTradeType());
|
||||
assertNotNull(request.getMerchantCategoryCode());
|
||||
assertEquals("JSAPI", request.getTradeType());
|
||||
assertEquals("5812", request.getMerchantCategoryCode());
|
||||
assertEquals(outTradeNo, request.getOutTradeNo());
|
||||
assertEquals("Test overseas payment", request.getDescription());
|
||||
assertEquals(100, request.getAmount().getTotal());
|
||||
assertEquals("test_openid", request.getPayer().getOpenid());
|
||||
|
||||
// Test JSON serialization contains the new fields
|
||||
String json = GSON.toJson(request);
|
||||
assertTrue(json.contains("trade_type"));
|
||||
assertTrue(json.contains("merchant_category_code"));
|
||||
assertTrue(json.contains("JSAPI"));
|
||||
assertTrue(json.contains("5812"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGlobalTradeTypeEnum() {
|
||||
// Test that all trade types have the correct global endpoints
|
||||
assertEquals("/global/v3/transactions/app", GlobalTradeTypeEnum.APP.getUrl());
|
||||
assertEquals("/global/v3/transactions/jsapi", GlobalTradeTypeEnum.JSAPI.getUrl());
|
||||
assertEquals("/global/v3/transactions/native", GlobalTradeTypeEnum.NATIVE.getUrl());
|
||||
assertEquals("/global/v3/transactions/h5", GlobalTradeTypeEnum.H5.getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGlobalTradeTypeEnumValues() {
|
||||
// Test that we have all the main trade types
|
||||
GlobalTradeTypeEnum[] tradeTypes = GlobalTradeTypeEnum.values();
|
||||
assertEquals(4, tradeTypes.length);
|
||||
|
||||
// Test that we can convert between enum name and TradeTypeEnum
|
||||
for (GlobalTradeTypeEnum globalType : tradeTypes) {
|
||||
// This tests that the enum names match between Global and regular TradeTypeEnum
|
||||
String name = globalType.name();
|
||||
assertNotNull(name);
|
||||
assertTrue(name.equals("APP") || name.equals("JSAPI") || name.equals("NATIVE") || name.equals("H5"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.github.binarywang.wxpay.service.impl;
|
||||
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3GlobalRequest;
|
||||
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
|
||||
import com.github.binarywang.wxpay.bean.result.enums.GlobalTradeTypeEnum;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
import me.chanjar.weixin.common.util.RandomUtils;
|
||||
|
||||
/**
|
||||
* 境外微信支付使用示例
|
||||
* Example usage for overseas WeChat Pay
|
||||
*
|
||||
* @author Binary Wang
|
||||
*/
|
||||
public class OverseasWxPayExample {
|
||||
|
||||
/**
|
||||
* 境外微信支付JSAPI下单示例
|
||||
* Example for overseas WeChat Pay JSAPI order creation
|
||||
*/
|
||||
public void createOverseasJsapiOrder(WxPayService payService) throws WxPayException {
|
||||
// 创建境外支付请求对象
|
||||
WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
|
||||
|
||||
// 设置基础订单信息
|
||||
request.setOutTradeNo(RandomUtils.getRandomStr()); // 商户订单号
|
||||
request.setDescription("境外商品购买"); // 商品描述
|
||||
request.setNotifyUrl("https://your-domain.com/notify"); // 支付通知地址
|
||||
|
||||
// 设置金额信息
|
||||
WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
|
||||
amount.setCurrency(WxPayConstants.CurrencyType.CNY); // 币种
|
||||
amount.setTotal(100); // 金额,单位为分
|
||||
request.setAmount(amount);
|
||||
|
||||
// 设置支付者信息
|
||||
WxPayUnifiedOrderV3GlobalRequest.Payer payer = new WxPayUnifiedOrderV3GlobalRequest.Payer();
|
||||
payer.setOpenid("用户的openid"); // 用户openid
|
||||
request.setPayer(payer);
|
||||
|
||||
// 设置境外支付必需的参数
|
||||
request.setTradeType("JSAPI"); // 交易类型
|
||||
request.setMerchantCategoryCode("5812"); // 商户类目代码,境外商户必填
|
||||
|
||||
// 可选:设置场景信息
|
||||
WxPayUnifiedOrderV3GlobalRequest.SceneInfo sceneInfo = new WxPayUnifiedOrderV3GlobalRequest.SceneInfo();
|
||||
sceneInfo.setPayerClientIp("用户IP地址");
|
||||
request.setSceneInfo(sceneInfo);
|
||||
|
||||
// 调用境外支付接口
|
||||
WxPayUnifiedOrderV3Result.JsapiResult result = payService.createOrderV3Global(
|
||||
GlobalTradeTypeEnum.JSAPI,
|
||||
request
|
||||
);
|
||||
|
||||
// 返回的result包含前端需要的支付参数
|
||||
System.out.println("支付参数:" + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 境外微信支付APP下单示例
|
||||
* Example for overseas WeChat Pay APP order creation
|
||||
*/
|
||||
public void createOverseasAppOrder(WxPayService payService) throws WxPayException {
|
||||
WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
|
||||
|
||||
// 设置基础信息
|
||||
request.setOutTradeNo(RandomUtils.getRandomStr());
|
||||
request.setDescription("境外APP商品购买");
|
||||
request.setNotifyUrl("https://your-domain.com/notify");
|
||||
|
||||
// 设置金额
|
||||
WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
|
||||
amount.setCurrency(WxPayConstants.CurrencyType.CNY);
|
||||
amount.setTotal(200); // 2元
|
||||
request.setAmount(amount);
|
||||
|
||||
// APP支付不需要设置payer.openid,但需要设置空的payer对象
|
||||
request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer());
|
||||
|
||||
// 境外支付必需参数
|
||||
request.setTradeType("APP");
|
||||
request.setMerchantCategoryCode("5812");
|
||||
|
||||
// 调用境外APP支付接口
|
||||
WxPayUnifiedOrderV3Result.AppResult result = payService.createOrderV3Global(
|
||||
GlobalTradeTypeEnum.APP,
|
||||
request
|
||||
);
|
||||
|
||||
System.out.println("APP支付参数:" + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 境外微信支付NATIVE下单示例
|
||||
* Example for overseas WeChat Pay NATIVE order creation
|
||||
*/
|
||||
public void createOverseasNativeOrder(WxPayService payService) throws WxPayException {
|
||||
WxPayUnifiedOrderV3GlobalRequest request = new WxPayUnifiedOrderV3GlobalRequest();
|
||||
|
||||
request.setOutTradeNo(RandomUtils.getRandomStr());
|
||||
request.setDescription("境外扫码支付");
|
||||
request.setNotifyUrl("https://your-domain.com/notify");
|
||||
|
||||
// 设置金额
|
||||
WxPayUnifiedOrderV3GlobalRequest.Amount amount = new WxPayUnifiedOrderV3GlobalRequest.Amount();
|
||||
amount.setCurrency(WxPayConstants.CurrencyType.CNY);
|
||||
amount.setTotal(300); // 3元
|
||||
request.setAmount(amount);
|
||||
|
||||
// NATIVE支付不需要设置payer.openid
|
||||
request.setPayer(new WxPayUnifiedOrderV3GlobalRequest.Payer());
|
||||
|
||||
// 境外支付必需参数
|
||||
request.setTradeType("NATIVE");
|
||||
request.setMerchantCategoryCode("5812");
|
||||
|
||||
// 调用境外NATIVE支付接口
|
||||
String result = payService.createOrderV3Global(
|
||||
GlobalTradeTypeEnum.NATIVE,
|
||||
request
|
||||
);
|
||||
|
||||
System.out.println("NATIVE支付二维码链接:" + result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置示例
|
||||
* Configuration example
|
||||
*/
|
||||
public WxPayConfig createOverseasConfig() {
|
||||
WxPayConfig config = new WxPayConfig();
|
||||
|
||||
// 基础配置
|
||||
config.setAppId("你的AppId");
|
||||
config.setMchId("你的境外商户号");
|
||||
config.setMchKey("你的商户密钥");
|
||||
config.setNotifyUrl("https://your-domain.com/notify");
|
||||
|
||||
// 境外支付使用的是全球API,在代码中会自动使用 https://apihk.mch.weixin.qq.com 作为基础URL
|
||||
// 无需额外设置payBaseUrl,方法内部会自动处理
|
||||
|
||||
// V3相关配置(境外支付也使用V3接口)
|
||||
config.setPrivateKeyPath("你的私钥文件路径");
|
||||
config.setCertSerialNo("你的商户证书序列号");
|
||||
config.setApiV3Key("你的APIv3密钥");
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user