@cloudcome/utils-uni 1.40.0 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/weixin/notice.d.ts +9 -10
- package/dist/weixin/token.d.ts +9 -9
- package/dist/weixin.cjs +20 -20
- package/dist/weixin.cjs.map +1 -1
- package/dist/weixin.mjs +19 -19
- package/dist/weixin.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
package/dist/index.mjs
CHANGED
package/dist/weixin/notice.d.ts
CHANGED
|
@@ -9,12 +9,11 @@ export type SendData<T> = {
|
|
|
9
9
|
*/
|
|
10
10
|
userId: string;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* - `develop`: 开发版
|
|
12
|
+
* 小程序跳转环境
|
|
14
13
|
* - `trial`: 体验版
|
|
15
|
-
* - `
|
|
14
|
+
* - `formal`: 正式版
|
|
16
15
|
*/
|
|
17
|
-
|
|
16
|
+
miniprogramState: 'trial' | 'formal';
|
|
18
17
|
/**
|
|
19
18
|
* 通知数据,key 对应模板字段(如 thing1, number1)
|
|
20
19
|
*/
|
|
@@ -27,7 +26,7 @@ export type SendData<T> = {
|
|
|
27
26
|
/**
|
|
28
27
|
* 构建微信订阅消息发送服务的选项
|
|
29
28
|
*/
|
|
30
|
-
export type
|
|
29
|
+
export type BuildWeixinNoticeSenderOptions = {
|
|
31
30
|
/**
|
|
32
31
|
* 订阅消息模板ID
|
|
33
32
|
*/
|
|
@@ -35,7 +34,7 @@ export type BuildSendWeixinNoticeServiceOptions = {
|
|
|
35
34
|
/**
|
|
36
35
|
* 获取微信 access_token 的服务函数
|
|
37
36
|
*/
|
|
38
|
-
|
|
37
|
+
getWeixinAccessToken: () => Promise<string>;
|
|
39
38
|
/**
|
|
40
39
|
* 根据用户ID获取微信 openId
|
|
41
40
|
* @param userId - 用户ID
|
|
@@ -59,9 +58,9 @@ export type BuildSendWeixinNoticeServiceOptions = {
|
|
|
59
58
|
*
|
|
60
59
|
* @example
|
|
61
60
|
* ```ts
|
|
62
|
-
* const sendNotice =
|
|
61
|
+
* const sendNotice = buildWeixinNoticeSender({
|
|
63
62
|
* templateId: 'tmpl_abc123',
|
|
64
|
-
*
|
|
63
|
+
* getWeixinAccessToken: getAccessToken,
|
|
65
64
|
* getUserWeixinOpenId: async (userId) => {
|
|
66
65
|
* const user = await db.collection('users').doc(userId).get()
|
|
67
66
|
* return user.data?.openId
|
|
@@ -70,10 +69,10 @@ export type BuildSendWeixinNoticeServiceOptions = {
|
|
|
70
69
|
*
|
|
71
70
|
* await sendNotice({
|
|
72
71
|
* userId: 'user-123',
|
|
73
|
-
*
|
|
72
|
+
* miniprogramState: 'formal',
|
|
74
73
|
* payload: { thing1: '订单已发货', number1: 12345 },
|
|
75
74
|
* page: '/pages/order/detail?id=12345',
|
|
76
75
|
* })
|
|
77
76
|
* ```
|
|
78
77
|
*/
|
|
79
|
-
export declare function
|
|
78
|
+
export declare function buildWeixinNoticeSender<T extends Record<string, number | string>>(options: BuildWeixinNoticeSenderOptions): (sendData: SendData<T>) => Promise<void>;
|
package/dist/weixin/token.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { request } from '../cloud';
|
|
|
2
2
|
/**
|
|
3
3
|
* 构建微信 access_token 获取服务的选项
|
|
4
4
|
*/
|
|
5
|
-
export type
|
|
5
|
+
export type BuildWeixinAccessTokenGetterOptions = {
|
|
6
6
|
/**
|
|
7
7
|
* 微信小程序应用ID
|
|
8
8
|
*/
|
|
@@ -12,16 +12,16 @@ export type BuildWeixinAccessTokenServiceOptions = {
|
|
|
12
12
|
*/
|
|
13
13
|
appSecret: string;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* 查询缓存的 access_token
|
|
16
16
|
* @returns 缓存的 access_token,无缓存时返回空字符串
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
queryAccessToken: () => Promise<string>;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* 保存 access_token 到缓存
|
|
21
21
|
* @param accessToken access_token 值
|
|
22
22
|
* @param expiresIn 过期时间,单位毫秒
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
saveAccessToken: (accessToken: string, expiresIn: number) => Promise<void>;
|
|
25
25
|
/**
|
|
26
26
|
* 模拟请求函数,用于单元测试注入
|
|
27
27
|
*/
|
|
@@ -37,13 +37,13 @@ export type BuildWeixinAccessTokenServiceOptions = {
|
|
|
37
37
|
*
|
|
38
38
|
* @example
|
|
39
39
|
* ```ts
|
|
40
|
-
* const getAccessToken =
|
|
40
|
+
* const getAccessToken = buildWeixinAccessTokenGetter({
|
|
41
41
|
* appId: 'wx123',
|
|
42
42
|
* appSecret: 'secret',
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* queryAccessToken: () => kv.get('token'),
|
|
44
|
+
* saveAccessToken: (token, ttl) => kv.set('token', token, ttl),
|
|
45
45
|
* })
|
|
46
46
|
* const token = await getAccessToken()
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
|
-
export declare function
|
|
49
|
+
export declare function buildWeixinAccessTokenGetter(options: BuildWeixinAccessTokenGetterOptions): () => Promise<string>;
|
package/dist/weixin.cjs
CHANGED
|
@@ -13,19 +13,19 @@ let _cloudcome_utils_core_try = require("@cloudcome/utils-core/try");
|
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```ts
|
|
16
|
-
* const getAccessToken =
|
|
16
|
+
* const getAccessToken = buildWeixinAccessTokenGetter({
|
|
17
17
|
* appId: 'wx123',
|
|
18
18
|
* appSecret: 'secret',
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* queryAccessToken: () => kv.get('token'),
|
|
20
|
+
* saveAccessToken: (token, ttl) => kv.set('token', token, ttl),
|
|
21
21
|
* })
|
|
22
22
|
* const token = await getAccessToken()
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
|
|
26
|
-
const { appId, appSecret, _mockRequest,
|
|
27
|
-
return async function
|
|
28
|
-
let accessToken = await
|
|
25
|
+
function buildWeixinAccessTokenGetter(options) {
|
|
26
|
+
const { appId, appSecret, _mockRequest, queryAccessToken, saveAccessToken } = options;
|
|
27
|
+
return async function getWeixinAccessToken() {
|
|
28
|
+
let accessToken = await queryAccessToken();
|
|
29
29
|
if (accessToken) return accessToken;
|
|
30
30
|
const { data: accessInfo } = await (_mockRequest || require_cloud.request)({
|
|
31
31
|
url: "https://api.weixin.qq.com/cgi-bin/token",
|
|
@@ -38,8 +38,8 @@ async function buildWeixinAccessTokenService(options) {
|
|
|
38
38
|
});
|
|
39
39
|
if (!accessInfo.access_token) throw new Error(accessInfo.errmsg || "获取 access_token 失败");
|
|
40
40
|
accessToken = accessInfo.access_token;
|
|
41
|
-
const [err] = await (0, _cloudcome_utils_core_try.tryFlatten)(
|
|
42
|
-
if (err) console.error("
|
|
41
|
+
const [err] = await (0, _cloudcome_utils_core_try.tryFlatten)(saveAccessToken(accessToken, accessInfo.expires_in * 1e3));
|
|
42
|
+
if (err) console.error("保存 access_token 失败", err);
|
|
43
43
|
return accessToken;
|
|
44
44
|
};
|
|
45
45
|
}
|
|
@@ -57,9 +57,9 @@ async function buildWeixinAccessTokenService(options) {
|
|
|
57
57
|
*
|
|
58
58
|
* @example
|
|
59
59
|
* ```ts
|
|
60
|
-
* const sendNotice =
|
|
60
|
+
* const sendNotice = buildWeixinNoticeSender({
|
|
61
61
|
* templateId: 'tmpl_abc123',
|
|
62
|
-
*
|
|
62
|
+
* getWeixinAccessToken: getAccessToken,
|
|
63
63
|
* getUserWeixinOpenId: async (userId) => {
|
|
64
64
|
* const user = await db.collection('users').doc(userId).get()
|
|
65
65
|
* return user.data?.openId
|
|
@@ -68,19 +68,19 @@ async function buildWeixinAccessTokenService(options) {
|
|
|
68
68
|
*
|
|
69
69
|
* await sendNotice({
|
|
70
70
|
* userId: 'user-123',
|
|
71
|
-
*
|
|
71
|
+
* miniprogramState: 'formal',
|
|
72
72
|
* payload: { thing1: '订单已发货', number1: 12345 },
|
|
73
73
|
* page: '/pages/order/detail?id=12345',
|
|
74
74
|
* })
|
|
75
75
|
* ```
|
|
76
76
|
*/
|
|
77
|
-
function
|
|
78
|
-
const { templateId,
|
|
79
|
-
return async function
|
|
80
|
-
const { userId, page, payload } = sendData;
|
|
77
|
+
function buildWeixinNoticeSender(options) {
|
|
78
|
+
const { templateId, getWeixinAccessToken, getUserWeixinOpenId, _mockRequest } = options;
|
|
79
|
+
return async function sendWeixinNotice(sendData) {
|
|
80
|
+
const { userId, page, payload, miniprogramState } = sendData;
|
|
81
81
|
const wxOpenId = await getUserWeixinOpenId(userId);
|
|
82
82
|
if (!wxOpenId) throw new Error("用户未绑定微信");
|
|
83
|
-
const accessToken = await
|
|
83
|
+
const accessToken = await getWeixinAccessToken();
|
|
84
84
|
const { data } = await (_mockRequest || require_cloud.request)({
|
|
85
85
|
url: `https://api.weixin.qq.com/cgi-bin/message/subscribe/send`,
|
|
86
86
|
method: "POST",
|
|
@@ -89,7 +89,7 @@ function buildSendWeixinNoticeService(options) {
|
|
|
89
89
|
touser: wxOpenId,
|
|
90
90
|
template_id: templateId,
|
|
91
91
|
page: page.replace(/^\//, ""),
|
|
92
|
-
miniprogram_state:
|
|
92
|
+
miniprogram_state: miniprogramState,
|
|
93
93
|
lang: "zh_CN",
|
|
94
94
|
data: (0, _cloudcome_utils_core_object.objectMap)(payload, (val, key) => ({ value: _fixPayloadValue(key, val) }))
|
|
95
95
|
}
|
|
@@ -126,7 +126,7 @@ function _autoEllipsis(val, len) {
|
|
|
126
126
|
return val.length > len ? `${val.slice(0, len - 3)}...` : val;
|
|
127
127
|
}
|
|
128
128
|
//#endregion
|
|
129
|
-
exports.
|
|
130
|
-
exports.
|
|
129
|
+
exports.buildWeixinAccessTokenGetter = buildWeixinAccessTokenGetter;
|
|
130
|
+
exports.buildWeixinNoticeSender = buildWeixinNoticeSender;
|
|
131
131
|
|
|
132
132
|
//# sourceMappingURL=weixin.cjs.map
|
package/dist/weixin.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"weixin.cjs","names":[],"sources":["../src/weixin/token.ts","../src/weixin/notice.ts"],"sourcesContent":["import { tryFlatten } from '@cloudcome/utils-core/try';\nimport { request } from '../cloud';\n\n/**\n * 构建微信 access_token 获取服务的选项\n */\nexport type
|
|
1
|
+
{"version":3,"file":"weixin.cjs","names":[],"sources":["../src/weixin/token.ts","../src/weixin/notice.ts"],"sourcesContent":["import { tryFlatten } from '@cloudcome/utils-core/try';\nimport { request } from '../cloud';\n\n/**\n * 构建微信 access_token 获取服务的选项\n */\nexport type BuildWeixinAccessTokenGetterOptions = {\n /**\n * 微信小程序应用ID\n */\n appId: string;\n\n /**\n * 微信小程序应用密钥\n */\n appSecret: string;\n\n /**\n * 查询缓存的 access_token\n * @returns 缓存的 access_token,无缓存时返回空字符串\n */\n queryAccessToken: () => Promise<string>;\n\n /**\n * 保存 access_token 到缓存\n * @param accessToken access_token 值\n * @param expiresIn 过期时间,单位毫秒\n */\n saveAccessToken: (accessToken: string, expiresIn: number) => Promise<void>;\n\n /**\n * 模拟请求函数,用于单元测试注入\n */\n _mockRequest?: typeof request;\n};\n\n/**\n * 构建微信 access_token 获取服务。\n *\n * 自动处理缓存逻辑:优先从临时数据中获取,不存在时调用微信 API 获取并缓存。\n *\n * @param options - 构造选项\n * @returns 获取 access_token 的异步函数\n *\n * @example\n * ```ts\n * const getAccessToken = buildWeixinAccessTokenGetter({\n * appId: 'wx123',\n * appSecret: 'secret',\n * queryAccessToken: () => kv.get('token'),\n * saveAccessToken: (token, ttl) => kv.set('token', token, ttl),\n * })\n * const token = await getAccessToken()\n * ```\n */\nexport function buildWeixinAccessTokenGetter(options: BuildWeixinAccessTokenGetterOptions) {\n const { appId, appSecret, _mockRequest, queryAccessToken, saveAccessToken } = options;\n\n return async function getWeixinAccessToken() {\n let accessToken = await queryAccessToken();\n if (accessToken) return accessToken;\n\n const { data: accessInfo } = await (_mockRequest || request)<{\n access_token: string;\n expires_in: number;\n errmsg: string;\n errcode: number;\n }>({\n url: 'https://api.weixin.qq.com/cgi-bin/token',\n method: 'GET',\n query: {\n grant_type: 'client_credential',\n appid: appId,\n secret: appSecret,\n },\n });\n\n if (!accessInfo.access_token) throw new Error(accessInfo.errmsg || '获取 access_token 失败');\n accessToken = accessInfo.access_token;\n\n const [err] = await tryFlatten(saveAccessToken(accessToken, accessInfo.expires_in * 1000));\n if (err) console.error('保存 access_token 失败', err);\n\n return accessToken;\n };\n}\n","import { request } from '@/cloud';\nimport { objectMap } from '@cloudcome/utils-core/object';\n\n/**\n * 发送微信订阅消息的数据结构\n * @template T - payload 字段类型,key 为模板字段名,value 为 string | number\n */\nexport type SendData<T> = {\n /**\n * 用户ID,用于查找对应的微信 openId\n */\n userId: string;\n\n /**\n * 小程序跳转环境\n * - `trial`: 体验版\n * - `formal`: 正式版\n */\n miniprogramState: 'trial' | 'formal';\n\n /**\n * 通知数据,key 对应模板字段(如 thing1, number1)\n */\n payload: T;\n\n /**\n * 点击消息后跳转的页面路径,开头 `/` 会被自动移除\n */\n page: string;\n};\n\n/**\n * 构建微信订阅消息发送服务的选项\n */\nexport type BuildWeixinNoticeSenderOptions = {\n /**\n * 订阅消息模板ID\n */\n templateId: string;\n\n /**\n * 获取微信 access_token 的服务函数\n */\n getWeixinAccessToken: () => Promise<string>;\n\n /**\n * 根据用户ID获取微信 openId\n * @param userId - 用户ID\n * @returns 微信 openId,未绑定时返回空字符串\n */\n getUserWeixinOpenId: (userId: string) => Promise<string>;\n\n /**\n * 模拟请求函数,用于单元测试注入\n */\n _mockRequest?: typeof request;\n};\n\n/**\n * 构建微信订阅消息发送服务。\n *\n * 封装微信订阅消息发送逻辑,自动处理 access_token 获取、openId 查找、\n * 字段长度截断(thing 类型 20 字符,character_string 类型 32 字符)等。\n *\n * @template T - payload 字段类型\n * @param options - 构造选项\n * @returns 发送订阅消息的函数\n *\n * @example\n * ```ts\n * const sendNotice = buildWeixinNoticeSender({\n * templateId: 'tmpl_abc123',\n * getWeixinAccessToken: getAccessToken,\n * getUserWeixinOpenId: async (userId) => {\n * const user = await db.collection('users').doc(userId).get()\n * return user.data?.openId\n * },\n * })\n *\n * await sendNotice({\n * userId: 'user-123',\n * miniprogramState: 'formal',\n * payload: { thing1: '订单已发货', number1: 12345 },\n * page: '/pages/order/detail?id=12345',\n * })\n * ```\n */\nexport function buildWeixinNoticeSender<T extends Record<string, number | string>>(\n options: BuildWeixinNoticeSenderOptions,\n) {\n const { templateId, getWeixinAccessToken, getUserWeixinOpenId, _mockRequest } = options;\n\n return async function sendWeixinNotice(sendData: SendData<T>) {\n const { userId, page, payload, miniprogramState } = sendData;\n const wxOpenId = await getUserWeixinOpenId(userId);\n if (!wxOpenId) throw new Error('用户未绑定微信');\n\n const accessToken = await getWeixinAccessToken();\n const { data } = await (_mockRequest || request)<{\n errcode: number;\n errmsg: string;\n }>({\n url: `https://api.weixin.qq.com/cgi-bin/message/subscribe/send`,\n method: 'POST',\n query: {\n access_token: accessToken,\n },\n data: {\n touser: wxOpenId,\n template_id: templateId,\n page: page.replace(/^\\//, ''),\n miniprogram_state: miniprogramState,\n lang: 'zh_CN',\n data: objectMap(payload, (val, key) => ({\n value: _fixPayloadValue(key as string, val),\n })),\n },\n });\n\n if (data.errcode === 43101) return;\n\n if (data.errcode !== 0) throw new Error(data.errmsg || '发送失败,未知错误');\n };\n}\n\n/**\n * 修复通知 payload 字段值,根据微信模板字段类型自动截断。\n *\n * 截断规则:\n * - thing 类型:20 字符以内\n * - character_string 类型:32 字符以内\n * - 其他类型:不处理\n *\n * @param key - 模板字段 key(如 thing1, number1)\n * @param val - 字段值\n * @returns 修复后的值\n */\nfunction _fixPayloadValue(key: string, val: number | string) {\n if (key.startsWith('thing')) return _autoEllipsis(val.toString(), 20);\n if (key.startsWith('character_string')) return _autoEllipsis(val.toString(), 32);\n return val;\n}\n\n/**\n * 字符串超长时自动截断并添加省略号。\n *\n * @param val - 原始字符串\n * @param len - 最大长度\n * @returns 截断后的字符串\n */\nfunction _autoEllipsis(val: string, len: number) {\n return val.length > len ? `${val.slice(0, len - 3)}...` : val;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,6BAA6B,SAA8C;CACzF,MAAM,EAAE,OAAO,WAAW,cAAc,kBAAkB,oBAAoB;CAE9E,OAAO,eAAe,uBAAuB;EAC3C,IAAI,cAAc,MAAM,iBAAiB;EACzC,IAAI,aAAa,OAAO;EAExB,MAAM,EAAE,MAAM,eAAe,OAAO,gBAAgB,cAAA,SAKjD;GACD,KAAK;GACL,QAAQ;GACR,OAAO;IACL,YAAY;IACZ,OAAO;IACP,QAAQ;GACV;EACF,CAAC;EAED,IAAI,CAAC,WAAW,cAAc,MAAM,IAAI,MAAM,WAAW,UAAU,oBAAoB;EACvF,cAAc,WAAW;EAEzB,MAAM,CAAC,OAAO,OAAA,GAAA,0BAAA,YAAiB,gBAAgB,aAAa,WAAW,aAAa,GAAI,CAAC;EACzF,IAAI,KAAK,QAAQ,MAAM,sBAAsB,GAAG;EAEhD,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACEA,SAAgB,wBACd,SACA;CACA,MAAM,EAAE,YAAY,sBAAsB,qBAAqB,iBAAiB;CAEhF,OAAO,eAAe,iBAAiB,UAAuB;EAC5D,MAAM,EAAE,QAAQ,MAAM,SAAS,qBAAqB;EACpD,MAAM,WAAW,MAAM,oBAAoB,MAAM;EACjD,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,SAAS;EAExC,MAAM,cAAc,MAAM,qBAAqB;EAC/C,MAAM,EAAE,SAAS,OAAO,gBAAgB,cAAA,SAGrC;GACD,KAAK;GACL,QAAQ;GACR,OAAO,EACL,cAAc,YAChB;GACA,MAAM;IACJ,QAAQ;IACR,aAAa;IACb,MAAM,KAAK,QAAQ,OAAO,EAAE;IAC5B,mBAAmB;IACnB,MAAM;IACN,OAAA,GAAA,6BAAA,WAAgB,UAAU,KAAK,SAAS,EACtC,OAAO,iBAAiB,KAAe,GAAG,EAC5C,EAAE;GACJ;EACF,CAAC;EAED,IAAI,KAAK,YAAY,OAAO;EAE5B,IAAI,KAAK,YAAY,GAAG,MAAM,IAAI,MAAM,KAAK,UAAU,WAAW;CACpE;AACF;;;;;;;;;;;;;AAcA,SAAS,iBAAiB,KAAa,KAAsB;CAC3D,IAAI,IAAI,WAAW,OAAO,GAAG,OAAO,cAAc,IAAI,SAAS,GAAG,EAAE;CACpE,IAAI,IAAI,WAAW,kBAAkB,GAAG,OAAO,cAAc,IAAI,SAAS,GAAG,EAAE;CAC/E,OAAO;AACT;;;;;;;;AASA,SAAS,cAAc,KAAa,KAAa;CAC/C,OAAO,IAAI,SAAS,MAAM,GAAG,IAAI,MAAM,GAAG,MAAM,CAAC,EAAE,OAAO;AAC5D"}
|
package/dist/weixin.mjs
CHANGED
|
@@ -12,19 +12,19 @@ import { tryFlatten } from "@cloudcome/utils-core/try";
|
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
14
|
* ```ts
|
|
15
|
-
* const getAccessToken =
|
|
15
|
+
* const getAccessToken = buildWeixinAccessTokenGetter({
|
|
16
16
|
* appId: 'wx123',
|
|
17
17
|
* appSecret: 'secret',
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* queryAccessToken: () => kv.get('token'),
|
|
19
|
+
* saveAccessToken: (token, ttl) => kv.set('token', token, ttl),
|
|
20
20
|
* })
|
|
21
21
|
* const token = await getAccessToken()
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
|
|
25
|
-
const { appId, appSecret, _mockRequest,
|
|
26
|
-
return async function
|
|
27
|
-
let accessToken = await
|
|
24
|
+
function buildWeixinAccessTokenGetter(options) {
|
|
25
|
+
const { appId, appSecret, _mockRequest, queryAccessToken, saveAccessToken } = options;
|
|
26
|
+
return async function getWeixinAccessToken() {
|
|
27
|
+
let accessToken = await queryAccessToken();
|
|
28
28
|
if (accessToken) return accessToken;
|
|
29
29
|
const { data: accessInfo } = await (_mockRequest || request)({
|
|
30
30
|
url: "https://api.weixin.qq.com/cgi-bin/token",
|
|
@@ -37,8 +37,8 @@ async function buildWeixinAccessTokenService(options) {
|
|
|
37
37
|
});
|
|
38
38
|
if (!accessInfo.access_token) throw new Error(accessInfo.errmsg || "获取 access_token 失败");
|
|
39
39
|
accessToken = accessInfo.access_token;
|
|
40
|
-
const [err] = await tryFlatten(
|
|
41
|
-
if (err) console.error("
|
|
40
|
+
const [err] = await tryFlatten(saveAccessToken(accessToken, accessInfo.expires_in * 1e3));
|
|
41
|
+
if (err) console.error("保存 access_token 失败", err);
|
|
42
42
|
return accessToken;
|
|
43
43
|
};
|
|
44
44
|
}
|
|
@@ -56,9 +56,9 @@ async function buildWeixinAccessTokenService(options) {
|
|
|
56
56
|
*
|
|
57
57
|
* @example
|
|
58
58
|
* ```ts
|
|
59
|
-
* const sendNotice =
|
|
59
|
+
* const sendNotice = buildWeixinNoticeSender({
|
|
60
60
|
* templateId: 'tmpl_abc123',
|
|
61
|
-
*
|
|
61
|
+
* getWeixinAccessToken: getAccessToken,
|
|
62
62
|
* getUserWeixinOpenId: async (userId) => {
|
|
63
63
|
* const user = await db.collection('users').doc(userId).get()
|
|
64
64
|
* return user.data?.openId
|
|
@@ -67,19 +67,19 @@ async function buildWeixinAccessTokenService(options) {
|
|
|
67
67
|
*
|
|
68
68
|
* await sendNotice({
|
|
69
69
|
* userId: 'user-123',
|
|
70
|
-
*
|
|
70
|
+
* miniprogramState: 'formal',
|
|
71
71
|
* payload: { thing1: '订单已发货', number1: 12345 },
|
|
72
72
|
* page: '/pages/order/detail?id=12345',
|
|
73
73
|
* })
|
|
74
74
|
* ```
|
|
75
75
|
*/
|
|
76
|
-
function
|
|
77
|
-
const { templateId,
|
|
78
|
-
return async function
|
|
79
|
-
const { userId, page, payload } = sendData;
|
|
76
|
+
function buildWeixinNoticeSender(options) {
|
|
77
|
+
const { templateId, getWeixinAccessToken, getUserWeixinOpenId, _mockRequest } = options;
|
|
78
|
+
return async function sendWeixinNotice(sendData) {
|
|
79
|
+
const { userId, page, payload, miniprogramState } = sendData;
|
|
80
80
|
const wxOpenId = await getUserWeixinOpenId(userId);
|
|
81
81
|
if (!wxOpenId) throw new Error("用户未绑定微信");
|
|
82
|
-
const accessToken = await
|
|
82
|
+
const accessToken = await getWeixinAccessToken();
|
|
83
83
|
const { data } = await (_mockRequest || request)({
|
|
84
84
|
url: `https://api.weixin.qq.com/cgi-bin/message/subscribe/send`,
|
|
85
85
|
method: "POST",
|
|
@@ -88,7 +88,7 @@ function buildSendWeixinNoticeService(options) {
|
|
|
88
88
|
touser: wxOpenId,
|
|
89
89
|
template_id: templateId,
|
|
90
90
|
page: page.replace(/^\//, ""),
|
|
91
|
-
miniprogram_state:
|
|
91
|
+
miniprogram_state: miniprogramState,
|
|
92
92
|
lang: "zh_CN",
|
|
93
93
|
data: objectMap(payload, (val, key) => ({ value: _fixPayloadValue(key, val) }))
|
|
94
94
|
}
|
|
@@ -125,6 +125,6 @@ function _autoEllipsis(val, len) {
|
|
|
125
125
|
return val.length > len ? `${val.slice(0, len - 3)}...` : val;
|
|
126
126
|
}
|
|
127
127
|
//#endregion
|
|
128
|
-
export {
|
|
128
|
+
export { buildWeixinAccessTokenGetter, buildWeixinNoticeSender };
|
|
129
129
|
|
|
130
130
|
//# sourceMappingURL=weixin.mjs.map
|
package/dist/weixin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"weixin.mjs","names":[],"sources":["../src/weixin/token.ts","../src/weixin/notice.ts"],"sourcesContent":["import { tryFlatten } from '@cloudcome/utils-core/try';\nimport { request } from '../cloud';\n\n/**\n * 构建微信 access_token 获取服务的选项\n */\nexport type
|
|
1
|
+
{"version":3,"file":"weixin.mjs","names":[],"sources":["../src/weixin/token.ts","../src/weixin/notice.ts"],"sourcesContent":["import { tryFlatten } from '@cloudcome/utils-core/try';\nimport { request } from '../cloud';\n\n/**\n * 构建微信 access_token 获取服务的选项\n */\nexport type BuildWeixinAccessTokenGetterOptions = {\n /**\n * 微信小程序应用ID\n */\n appId: string;\n\n /**\n * 微信小程序应用密钥\n */\n appSecret: string;\n\n /**\n * 查询缓存的 access_token\n * @returns 缓存的 access_token,无缓存时返回空字符串\n */\n queryAccessToken: () => Promise<string>;\n\n /**\n * 保存 access_token 到缓存\n * @param accessToken access_token 值\n * @param expiresIn 过期时间,单位毫秒\n */\n saveAccessToken: (accessToken: string, expiresIn: number) => Promise<void>;\n\n /**\n * 模拟请求函数,用于单元测试注入\n */\n _mockRequest?: typeof request;\n};\n\n/**\n * 构建微信 access_token 获取服务。\n *\n * 自动处理缓存逻辑:优先从临时数据中获取,不存在时调用微信 API 获取并缓存。\n *\n * @param options - 构造选项\n * @returns 获取 access_token 的异步函数\n *\n * @example\n * ```ts\n * const getAccessToken = buildWeixinAccessTokenGetter({\n * appId: 'wx123',\n * appSecret: 'secret',\n * queryAccessToken: () => kv.get('token'),\n * saveAccessToken: (token, ttl) => kv.set('token', token, ttl),\n * })\n * const token = await getAccessToken()\n * ```\n */\nexport function buildWeixinAccessTokenGetter(options: BuildWeixinAccessTokenGetterOptions) {\n const { appId, appSecret, _mockRequest, queryAccessToken, saveAccessToken } = options;\n\n return async function getWeixinAccessToken() {\n let accessToken = await queryAccessToken();\n if (accessToken) return accessToken;\n\n const { data: accessInfo } = await (_mockRequest || request)<{\n access_token: string;\n expires_in: number;\n errmsg: string;\n errcode: number;\n }>({\n url: 'https://api.weixin.qq.com/cgi-bin/token',\n method: 'GET',\n query: {\n grant_type: 'client_credential',\n appid: appId,\n secret: appSecret,\n },\n });\n\n if (!accessInfo.access_token) throw new Error(accessInfo.errmsg || '获取 access_token 失败');\n accessToken = accessInfo.access_token;\n\n const [err] = await tryFlatten(saveAccessToken(accessToken, accessInfo.expires_in * 1000));\n if (err) console.error('保存 access_token 失败', err);\n\n return accessToken;\n };\n}\n","import { request } from '@/cloud';\nimport { objectMap } from '@cloudcome/utils-core/object';\n\n/**\n * 发送微信订阅消息的数据结构\n * @template T - payload 字段类型,key 为模板字段名,value 为 string | number\n */\nexport type SendData<T> = {\n /**\n * 用户ID,用于查找对应的微信 openId\n */\n userId: string;\n\n /**\n * 小程序跳转环境\n * - `trial`: 体验版\n * - `formal`: 正式版\n */\n miniprogramState: 'trial' | 'formal';\n\n /**\n * 通知数据,key 对应模板字段(如 thing1, number1)\n */\n payload: T;\n\n /**\n * 点击消息后跳转的页面路径,开头 `/` 会被自动移除\n */\n page: string;\n};\n\n/**\n * 构建微信订阅消息发送服务的选项\n */\nexport type BuildWeixinNoticeSenderOptions = {\n /**\n * 订阅消息模板ID\n */\n templateId: string;\n\n /**\n * 获取微信 access_token 的服务函数\n */\n getWeixinAccessToken: () => Promise<string>;\n\n /**\n * 根据用户ID获取微信 openId\n * @param userId - 用户ID\n * @returns 微信 openId,未绑定时返回空字符串\n */\n getUserWeixinOpenId: (userId: string) => Promise<string>;\n\n /**\n * 模拟请求函数,用于单元测试注入\n */\n _mockRequest?: typeof request;\n};\n\n/**\n * 构建微信订阅消息发送服务。\n *\n * 封装微信订阅消息发送逻辑,自动处理 access_token 获取、openId 查找、\n * 字段长度截断(thing 类型 20 字符,character_string 类型 32 字符)等。\n *\n * @template T - payload 字段类型\n * @param options - 构造选项\n * @returns 发送订阅消息的函数\n *\n * @example\n * ```ts\n * const sendNotice = buildWeixinNoticeSender({\n * templateId: 'tmpl_abc123',\n * getWeixinAccessToken: getAccessToken,\n * getUserWeixinOpenId: async (userId) => {\n * const user = await db.collection('users').doc(userId).get()\n * return user.data?.openId\n * },\n * })\n *\n * await sendNotice({\n * userId: 'user-123',\n * miniprogramState: 'formal',\n * payload: { thing1: '订单已发货', number1: 12345 },\n * page: '/pages/order/detail?id=12345',\n * })\n * ```\n */\nexport function buildWeixinNoticeSender<T extends Record<string, number | string>>(\n options: BuildWeixinNoticeSenderOptions,\n) {\n const { templateId, getWeixinAccessToken, getUserWeixinOpenId, _mockRequest } = options;\n\n return async function sendWeixinNotice(sendData: SendData<T>) {\n const { userId, page, payload, miniprogramState } = sendData;\n const wxOpenId = await getUserWeixinOpenId(userId);\n if (!wxOpenId) throw new Error('用户未绑定微信');\n\n const accessToken = await getWeixinAccessToken();\n const { data } = await (_mockRequest || request)<{\n errcode: number;\n errmsg: string;\n }>({\n url: `https://api.weixin.qq.com/cgi-bin/message/subscribe/send`,\n method: 'POST',\n query: {\n access_token: accessToken,\n },\n data: {\n touser: wxOpenId,\n template_id: templateId,\n page: page.replace(/^\\//, ''),\n miniprogram_state: miniprogramState,\n lang: 'zh_CN',\n data: objectMap(payload, (val, key) => ({\n value: _fixPayloadValue(key as string, val),\n })),\n },\n });\n\n if (data.errcode === 43101) return;\n\n if (data.errcode !== 0) throw new Error(data.errmsg || '发送失败,未知错误');\n };\n}\n\n/**\n * 修复通知 payload 字段值,根据微信模板字段类型自动截断。\n *\n * 截断规则:\n * - thing 类型:20 字符以内\n * - character_string 类型:32 字符以内\n * - 其他类型:不处理\n *\n * @param key - 模板字段 key(如 thing1, number1)\n * @param val - 字段值\n * @returns 修复后的值\n */\nfunction _fixPayloadValue(key: string, val: number | string) {\n if (key.startsWith('thing')) return _autoEllipsis(val.toString(), 20);\n if (key.startsWith('character_string')) return _autoEllipsis(val.toString(), 32);\n return val;\n}\n\n/**\n * 字符串超长时自动截断并添加省略号。\n *\n * @param val - 原始字符串\n * @param len - 最大长度\n * @returns 截断后的字符串\n */\nfunction _autoEllipsis(val: string, len: number) {\n return val.length > len ? `${val.slice(0, len - 3)}...` : val;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,6BAA6B,SAA8C;CACzF,MAAM,EAAE,OAAO,WAAW,cAAc,kBAAkB,oBAAoB;CAE9E,OAAO,eAAe,uBAAuB;EAC3C,IAAI,cAAc,MAAM,iBAAiB;EACzC,IAAI,aAAa,OAAO;EAExB,MAAM,EAAE,MAAM,eAAe,OAAO,gBAAgB,SAKjD;GACD,KAAK;GACL,QAAQ;GACR,OAAO;IACL,YAAY;IACZ,OAAO;IACP,QAAQ;GACV;EACF,CAAC;EAED,IAAI,CAAC,WAAW,cAAc,MAAM,IAAI,MAAM,WAAW,UAAU,oBAAoB;EACvF,cAAc,WAAW;EAEzB,MAAM,CAAC,OAAO,MAAM,WAAW,gBAAgB,aAAa,WAAW,aAAa,GAAI,CAAC;EACzF,IAAI,KAAK,QAAQ,MAAM,sBAAsB,GAAG;EAEhD,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACEA,SAAgB,wBACd,SACA;CACA,MAAM,EAAE,YAAY,sBAAsB,qBAAqB,iBAAiB;CAEhF,OAAO,eAAe,iBAAiB,UAAuB;EAC5D,MAAM,EAAE,QAAQ,MAAM,SAAS,qBAAqB;EACpD,MAAM,WAAW,MAAM,oBAAoB,MAAM;EACjD,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,SAAS;EAExC,MAAM,cAAc,MAAM,qBAAqB;EAC/C,MAAM,EAAE,SAAS,OAAO,gBAAgB,SAGrC;GACD,KAAK;GACL,QAAQ;GACR,OAAO,EACL,cAAc,YAChB;GACA,MAAM;IACJ,QAAQ;IACR,aAAa;IACb,MAAM,KAAK,QAAQ,OAAO,EAAE;IAC5B,mBAAmB;IACnB,MAAM;IACN,MAAM,UAAU,UAAU,KAAK,SAAS,EACtC,OAAO,iBAAiB,KAAe,GAAG,EAC5C,EAAE;GACJ;EACF,CAAC;EAED,IAAI,KAAK,YAAY,OAAO;EAE5B,IAAI,KAAK,YAAY,GAAG,MAAM,IAAI,MAAM,KAAK,UAAU,WAAW;CACpE;AACF;;;;;;;;;;;;;AAcA,SAAS,iBAAiB,KAAa,KAAsB;CAC3D,IAAI,IAAI,WAAW,OAAO,GAAG,OAAO,cAAc,IAAI,SAAS,GAAG,EAAE;CACpE,IAAI,IAAI,WAAW,kBAAkB,GAAG,OAAO,cAAc,IAAI,SAAS,GAAG,EAAE;CAC/E,OAAO;AACT;;;;;;;;AASA,SAAS,cAAc,KAAa,KAAa;CAC/C,OAAO,IAAI,SAAS,MAAM,GAAG,IAAI,MAAM,GAAG,MAAM,CAAC,EAAE,OAAO;AAC5D"}
|