feat(wechat): 关注公众号后自动回复欢迎语(被动回复,可后台配置)
用户关注服务号(扫登录码后关注 或 直接搜索关注)时,在回调 HTTP 响应里 回一条文本消息(微信「被动回复」)。被动回复不需要 access_token、不受 IP 白名单 限制,永远能发。欢迎语在管理端「登录设置」可配,留空用默认。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,48 @@ func TestParseEvent_IgnoresNonLogin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 关注事件要能识别(自动回复欢迎语),并解析出回复所需的 openid + 公众号原始 ID。
|
||||
func TestParseEvent_SubscribeReplyFields(t *testing.T) {
|
||||
sub := `<xml><ToUserName><![CDATA[gh_808ba17576cf]]></ToUserName>
|
||||
<FromUserName><![CDATA[openid_x]]></FromUserName>
|
||||
<CreateTime>1700000000</CreateTime>
|
||||
<MsgType><![CDATA[event]]></MsgType>
|
||||
<Event><![CDATA[subscribe]]></Event>
|
||||
<EventKey><![CDATA[]]></EventKey></xml>`
|
||||
ev, err := ParseEvent([]byte(sub))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ev.IsSubscribe() {
|
||||
t.Fatal("subscribe 应被识别为新关注")
|
||||
}
|
||||
if ev.ToUserName != "gh_808ba17576cf" {
|
||||
t.Fatalf("公众号原始 ID 取错:%q", ev.ToUserName)
|
||||
}
|
||||
// 无 scene 的直接关注:不是登录扫码,但仍是 subscribe(要回欢迎语)
|
||||
if ev.IsLoginScan() {
|
||||
t.Fatal("无 scene 关注不该触发登录")
|
||||
}
|
||||
}
|
||||
|
||||
// 被动回复 XML:ToUser=用户、FromUser=公众号、含文本内容,且防 CDATA 提前闭合。
|
||||
func TestBuildTextReply(t *testing.T) {
|
||||
xml := BuildTextReply("openid_user", "gh_pub", "你好]]>危险", 1700000000)
|
||||
for _, want := range []string{
|
||||
"<ToUserName><![CDATA[openid_user]]>",
|
||||
"<FromUserName><![CDATA[gh_pub]]>",
|
||||
"<MsgType><![CDATA[text]]>",
|
||||
"<CreateTime>1700000000</CreateTime>",
|
||||
} {
|
||||
if !strings.Contains(xml, want) {
|
||||
t.Fatalf("回复 XML 缺 %q:\n%s", want, xml)
|
||||
}
|
||||
}
|
||||
if strings.Contains(xml, "你好]]>危险") {
|
||||
t.Fatal("内容里的 ]]> 应被转义,避免 CDATA 提前闭合")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig_SecretRoundTrip(t *testing.T) {
|
||||
c := Config{AppID: "x", AppSecret: "plain-secret", Token: "t"}
|
||||
stored, err := c.EncryptedForStore()
|
||||
|
||||
Reference in New Issue
Block a user