@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 CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  /**
4
4
  * `@cloudcome/utils-uni` 版本号
5
5
  */
6
- var VERSION = "1.40.0";
6
+ var VERSION = "1.42.0";
7
7
  //#endregion
8
8
  exports.VERSION = VERSION;
9
9
 
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * `@cloudcome/utils-uni` 版本号
4
4
  */
5
- var VERSION = "1.40.0";
5
+ var VERSION = "1.42.0";
6
6
  //#endregion
7
7
  export { VERSION };
8
8
 
@@ -9,12 +9,11 @@ export type SendData<T> = {
9
9
  */
10
10
  userId: string;
11
11
  /**
12
- * 小程序环境
13
- * - `develop`: 开发版
12
+ * 小程序跳转环境
14
13
  * - `trial`: 体验版
15
- * - `release`: 正式版
14
+ * - `formal`: 正式版
16
15
  */
17
- clientEnv: 'develop' | 'trial' | 'release';
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 BuildSendWeixinNoticeServiceOptions = {
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
- getWeixinAccessTokenService: () => Promise<string>;
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 = buildSendWeixinNoticeService({
61
+ * const sendNotice = buildWeixinNoticeSender({
63
62
  * templateId: 'tmpl_abc123',
64
- * getWeixinAccessTokenService: getAccessToken,
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
- * clientEnv: 'release',
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 buildSendWeixinNoticeService<T extends Record<string, number | string>>(options: BuildSendWeixinNoticeServiceOptions): (sendData: SendData<T>) => Promise<void>;
78
+ export declare function buildWeixinNoticeSender<T extends Record<string, number | string>>(options: BuildWeixinNoticeSenderOptions): (sendData: SendData<T>) => Promise<void>;
@@ -2,7 +2,7 @@ import { request } from '../cloud';
2
2
  /**
3
3
  * 构建微信 access_token 获取服务的选项
4
4
  */
5
- export type BuildWeixinAccessTokenServiceOptions = {
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
- * 获取临时数据(用于缓存 access_token
15
+ * 查询缓存的 access_token
16
16
  * @returns 缓存的 access_token,无缓存时返回空字符串
17
17
  */
18
- getTempDataService: () => Promise<string>;
18
+ queryAccessToken: () => Promise<string>;
19
19
  /**
20
- * 设置临时数据(用于缓存 access_token
20
+ * 保存 access_token 到缓存
21
21
  * @param accessToken access_token 值
22
22
  * @param expiresIn 过期时间,单位毫秒
23
23
  */
24
- setTempDataService: (accessToken: string, expiresIn: number) => Promise<void>;
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 = await buildWeixinAccessTokenService({
40
+ * const getAccessToken = buildWeixinAccessTokenGetter({
41
41
  * appId: 'wx123',
42
42
  * appSecret: 'secret',
43
- * getTempDataService: () => kv.get('token'),
44
- * setTempDataService: (token, ttl) => kv.set('token', token, ttl),
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 buildWeixinAccessTokenService(options: BuildWeixinAccessTokenServiceOptions): Promise<() => Promise<string>>;
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 = await buildWeixinAccessTokenService({
16
+ * const getAccessToken = buildWeixinAccessTokenGetter({
17
17
  * appId: 'wx123',
18
18
  * appSecret: 'secret',
19
- * getTempDataService: () => kv.get('token'),
20
- * setTempDataService: (token, ttl) => kv.set('token', token, ttl),
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
- async function buildWeixinAccessTokenService(options) {
26
- const { appId, appSecret, _mockRequest, getTempDataService, setTempDataService } = options;
27
- return async function getWeixinAccessTokenService() {
28
- let accessToken = await getTempDataService();
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)(setTempDataService(accessToken, accessInfo.expires_in * 1e3));
42
- if (err) console.error("设置临时数据失败", err);
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 = buildSendWeixinNoticeService({
60
+ * const sendNotice = buildWeixinNoticeSender({
61
61
  * templateId: 'tmpl_abc123',
62
- * getWeixinAccessTokenService: getAccessToken,
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
- * clientEnv: 'release',
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 buildSendWeixinNoticeService(options) {
78
- const { templateId, getWeixinAccessTokenService, getUserWeixinOpenId, _mockRequest } = options;
79
- return async function sendWeixinNoticeService(sendData) {
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 getWeixinAccessTokenService();
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: sendData.clientEnv === "trial" ? "trial" : "formal",
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.buildSendWeixinNoticeService = buildSendWeixinNoticeService;
130
- exports.buildWeixinAccessTokenService = buildWeixinAccessTokenService;
129
+ exports.buildWeixinAccessTokenGetter = buildWeixinAccessTokenGetter;
130
+ exports.buildWeixinNoticeSender = buildWeixinNoticeSender;
131
131
 
132
132
  //# sourceMappingURL=weixin.cjs.map
@@ -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 BuildWeixinAccessTokenServiceOptions = {\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 getTempDataService: () => Promise<string>;\n\n /**\n * 设置临时数据(用于缓存 access_token)\n * @param accessToken access_token 值\n * @param expiresIn 过期时间,单位毫秒\n */\n setTempDataService: (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 = await buildWeixinAccessTokenService({\n * appId: 'wx123',\n * appSecret: 'secret',\n * getTempDataService: () => kv.get('token'),\n * setTempDataService: (token, ttl) => kv.set('token', token, ttl),\n * })\n * const token = await getAccessToken()\n * ```\n */\nexport async function buildWeixinAccessTokenService(options: BuildWeixinAccessTokenServiceOptions) {\n const { appId, appSecret, _mockRequest, getTempDataService, setTempDataService } = options;\n\n return async function getWeixinAccessTokenService() {\n let accessToken = await getTempDataService();\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(setTempDataService(accessToken, accessInfo.expires_in * 1000));\n if (err) console.error('设置临时数据失败', 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 * - `develop`: 开发版\n * - `trial`: 体验版\n * - `release`: 正式版\n */\n clientEnv: 'develop' | 'trial' | 'release';\n\n /**\n * 通知数据,key 对应模板字段(如 thing1, number1)\n */\n payload: T;\n\n /**\n * 点击消息后跳转的页面路径,开头 `/` 会被自动移除\n */\n page: string;\n};\n\n/**\n * 构建微信订阅消息发送服务的选项\n */\nexport type BuildSendWeixinNoticeServiceOptions = {\n /**\n * 订阅消息模板ID\n */\n templateId: string;\n\n /**\n * 获取微信 access_token 的服务函数\n */\n getWeixinAccessTokenService: () => 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 = buildSendWeixinNoticeService({\n * templateId: 'tmpl_abc123',\n * getWeixinAccessTokenService: 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 * clientEnv: 'release',\n * payload: { thing1: '订单已发货', number1: 12345 },\n * page: '/pages/order/detail?id=12345',\n * })\n * ```\n */\nexport function buildSendWeixinNoticeService<T extends Record<string, number | string>>(\n options: BuildSendWeixinNoticeServiceOptions,\n) {\n const { templateId, getWeixinAccessTokenService, getUserWeixinOpenId, _mockRequest } = options;\n\n return async function sendWeixinNoticeService(sendData: SendData<T>) {\n const { userId, page, payload } = sendData;\n const wxOpenId = await getUserWeixinOpenId(userId);\n if (!wxOpenId) throw new Error('用户未绑定微信');\n\n const accessToken = await getWeixinAccessTokenService();\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: sendData.clientEnv === 'trial' ? 'trial' : 'formal',\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,eAAsB,8BAA8B,SAA+C;CACjG,MAAM,EAAE,OAAO,WAAW,cAAc,oBAAoB,uBAAuB;CAEnF,OAAO,eAAe,8BAA8B;EAClD,IAAI,cAAc,MAAM,mBAAmB;EAC3C,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,mBAAmB,aAAa,WAAW,aAAa,GAAI,CAAC;EAC5F,IAAI,KAAK,QAAQ,MAAM,YAAY,GAAG;EAEtC,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACGA,SAAgB,6BACd,SACA;CACA,MAAM,EAAE,YAAY,6BAA6B,qBAAqB,iBAAiB;CAEvF,OAAO,eAAe,wBAAwB,UAAuB;EACnE,MAAM,EAAE,QAAQ,MAAM,YAAY;EAClC,MAAM,WAAW,MAAM,oBAAoB,MAAM;EACjD,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,SAAS;EAExC,MAAM,cAAc,MAAM,4BAA4B;EACtD,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,SAAS,cAAc,UAAU,UAAU;IAC9D,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"}
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 = await buildWeixinAccessTokenService({
15
+ * const getAccessToken = buildWeixinAccessTokenGetter({
16
16
  * appId: 'wx123',
17
17
  * appSecret: 'secret',
18
- * getTempDataService: () => kv.get('token'),
19
- * setTempDataService: (token, ttl) => kv.set('token', token, ttl),
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
- async function buildWeixinAccessTokenService(options) {
25
- const { appId, appSecret, _mockRequest, getTempDataService, setTempDataService } = options;
26
- return async function getWeixinAccessTokenService() {
27
- let accessToken = await getTempDataService();
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(setTempDataService(accessToken, accessInfo.expires_in * 1e3));
41
- if (err) console.error("设置临时数据失败", err);
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 = buildSendWeixinNoticeService({
59
+ * const sendNotice = buildWeixinNoticeSender({
60
60
  * templateId: 'tmpl_abc123',
61
- * getWeixinAccessTokenService: getAccessToken,
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
- * clientEnv: 'release',
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 buildSendWeixinNoticeService(options) {
77
- const { templateId, getWeixinAccessTokenService, getUserWeixinOpenId, _mockRequest } = options;
78
- return async function sendWeixinNoticeService(sendData) {
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 getWeixinAccessTokenService();
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: sendData.clientEnv === "trial" ? "trial" : "formal",
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 { buildSendWeixinNoticeService, buildWeixinAccessTokenService };
128
+ export { buildWeixinAccessTokenGetter, buildWeixinNoticeSender };
129
129
 
130
130
  //# sourceMappingURL=weixin.mjs.map
@@ -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 BuildWeixinAccessTokenServiceOptions = {\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 getTempDataService: () => Promise<string>;\n\n /**\n * 设置临时数据(用于缓存 access_token)\n * @param accessToken access_token 值\n * @param expiresIn 过期时间,单位毫秒\n */\n setTempDataService: (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 = await buildWeixinAccessTokenService({\n * appId: 'wx123',\n * appSecret: 'secret',\n * getTempDataService: () => kv.get('token'),\n * setTempDataService: (token, ttl) => kv.set('token', token, ttl),\n * })\n * const token = await getAccessToken()\n * ```\n */\nexport async function buildWeixinAccessTokenService(options: BuildWeixinAccessTokenServiceOptions) {\n const { appId, appSecret, _mockRequest, getTempDataService, setTempDataService } = options;\n\n return async function getWeixinAccessTokenService() {\n let accessToken = await getTempDataService();\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(setTempDataService(accessToken, accessInfo.expires_in * 1000));\n if (err) console.error('设置临时数据失败', 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 * - `develop`: 开发版\n * - `trial`: 体验版\n * - `release`: 正式版\n */\n clientEnv: 'develop' | 'trial' | 'release';\n\n /**\n * 通知数据,key 对应模板字段(如 thing1, number1)\n */\n payload: T;\n\n /**\n * 点击消息后跳转的页面路径,开头 `/` 会被自动移除\n */\n page: string;\n};\n\n/**\n * 构建微信订阅消息发送服务的选项\n */\nexport type BuildSendWeixinNoticeServiceOptions = {\n /**\n * 订阅消息模板ID\n */\n templateId: string;\n\n /**\n * 获取微信 access_token 的服务函数\n */\n getWeixinAccessTokenService: () => 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 = buildSendWeixinNoticeService({\n * templateId: 'tmpl_abc123',\n * getWeixinAccessTokenService: 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 * clientEnv: 'release',\n * payload: { thing1: '订单已发货', number1: 12345 },\n * page: '/pages/order/detail?id=12345',\n * })\n * ```\n */\nexport function buildSendWeixinNoticeService<T extends Record<string, number | string>>(\n options: BuildSendWeixinNoticeServiceOptions,\n) {\n const { templateId, getWeixinAccessTokenService, getUserWeixinOpenId, _mockRequest } = options;\n\n return async function sendWeixinNoticeService(sendData: SendData<T>) {\n const { userId, page, payload } = sendData;\n const wxOpenId = await getUserWeixinOpenId(userId);\n if (!wxOpenId) throw new Error('用户未绑定微信');\n\n const accessToken = await getWeixinAccessTokenService();\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: sendData.clientEnv === 'trial' ? 'trial' : 'formal',\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,eAAsB,8BAA8B,SAA+C;CACjG,MAAM,EAAE,OAAO,WAAW,cAAc,oBAAoB,uBAAuB;CAEnF,OAAO,eAAe,8BAA8B;EAClD,IAAI,cAAc,MAAM,mBAAmB;EAC3C,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,mBAAmB,aAAa,WAAW,aAAa,GAAI,CAAC;EAC5F,IAAI,KAAK,QAAQ,MAAM,YAAY,GAAG;EAEtC,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACGA,SAAgB,6BACd,SACA;CACA,MAAM,EAAE,YAAY,6BAA6B,qBAAqB,iBAAiB;CAEvF,OAAO,eAAe,wBAAwB,UAAuB;EACnE,MAAM,EAAE,QAAQ,MAAM,YAAY;EAClC,MAAM,WAAW,MAAM,oBAAoB,MAAM;EACjD,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,SAAS;EAExC,MAAM,cAAc,MAAM,4BAA4B;EACtD,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,SAAS,cAAc,UAAU,UAAU;IAC9D,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"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcome/utils-uni",
3
- "version": "1.40.0",
3
+ "version": "1.42.0",
4
4
  "description": "cloudcome utils for uni-app",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",