Tai.WeCom.EntityFrameworkCore
2.2.0
dotnet add package Tai.WeCom.EntityFrameworkCore --version 2.2.0
NuGet\Install-Package Tai.WeCom.EntityFrameworkCore -Version 2.2.0
<PackageReference Include="Tai.WeCom.EntityFrameworkCore" Version="2.2.0" />
<PackageVersion Include="Tai.WeCom.EntityFrameworkCore" Version="2.2.0" />
<PackageReference Include="Tai.WeCom.EntityFrameworkCore" />
paket add Tai.WeCom.EntityFrameworkCore --version 2.2.0
#r "nuget: Tai.WeCom.EntityFrameworkCore, 2.2.0"
#:package Tai.WeCom.EntityFrameworkCore@2.2.0
#addin nuget:?package=Tai.WeCom.EntityFrameworkCore&version=2.2.0
#tool nuget:?package=Tai.WeCom.EntityFrameworkCore&version=2.2.0
Tai.WeCom
自动发布使用模块版本标签和 NuGet Trusted Publishing,配置与操作见发布指南。
ABP 可复用模块,为企业微信(WeCom)提供外部联系人、客户群的数据同步与管理功能。
WeCom 是 tenant-only 模块:所有业务数据和凭据都必须属于具体租户,不支持 Host 侧使用。
特性
- 外部联系人同步 — 通过企业微信 API 全量拉取外部联系人(客户)数据,支持分页批量获取
- 客户群同步 — 全量同步客户群(外部群)信息及群成员,支持游标分页
- 回调事件处理 — 接收企业微信回调事件(URL 验证 + 事件接收),AES 解密与签名校验
- 增量更新 — 通过
change_external_contact/change_external_chat回调事件实现联系人、客户群的增量同步 - 变更日志审计 — 记录外部联系人与客户群的每一次变更(新增、编辑、删除、解散等),支持回溯
- 同步任务管理 — 可触发、查询全量同步任务,跟踪进度(Pending → Running → Completed/Failed)
- Access Token 管理 — 自动获取、缓存和提前刷新企业微信 Access Token
- 客户联系群发 — 创建、提醒、停止企业群发;群发记录、成员任务和发送结果持久化,支持定时同步和分页查询
- 客户朋友圈 — 创建文本/图片/视频/链接发表任务,按成员、部门和客户标签限定范围;停止发表,持久化成员发表情况和可见客户
- 定时发送与截止日期 — 群发和朋友圈均可预设发送时间与截止时间,到点自动创建、到期自动停止
- 朋友圈规则组与企业客户标签 — 透传管理企业微信规则组及客户标签,不新增本地表
- 失败回调重试 — 支持手动触发失败回调事件的重新处理
项目结构
src/
├── Tai.WeCom.Domain.Shared # 常量、枚举、错误码、本地化
├── Tai.WeCom.Domain # 实体、领域服务、API 客户端、Token 服务、加解密、Setting 定义
├── Tai.WeCom.Application.Contracts # 应用服务接口与 DTO
├── Tai.WeCom.Application # 应用服务实现、后台同步任务
├── Tai.WeCom.EntityFrameworkCore # EF Core DbContext、实体映射、仓储实现
├── Tai.WeCom.HttpApi # API 控制器
├── Tai.WeCom.HttpApi.Client # C# HTTP 客户端代理
└── Tai.WeCom.Installer # 便捷安装模块(聚合所有依赖)
test/
├── Tai.WeCom.TestBase
├── Tai.WeCom.Domain.Tests
├── Tai.WeCom.Application.Tests
└── Tai.WeCom.EntityFrameworkCore.Tests
快速开始
1. 安装模块
将以下项目引用添加到宿主应用的对应层:
dotnet add package Tai.WeCom.Application
dotnet add package Tai.WeCom.HttpApi
# 仅需要客户端代理时
dotnet add package Tai.WeCom.HttpApi.Client
在宿主模块中添加 DependsOn:
[DependsOn(typeof(TaiWeComApplicationModule))]
[DependsOn(typeof(TaiWeComHttpApiModule))]
public class YourHostModule : AbpModule { }
也可使用
TaiWeComInstallerModule(已聚合上述所有模块依赖):
[DependsOn(typeof(TaiWeComInstallerModule))]
public class YourHostModule : AbpModule { }
2. 配置 Settings
在目标租户上下文中,通过 ABP Setting Management 配置以下项。模块只读取 Tenant Setting
和代码内默认值,不会回退到 Host、Global 或 appsettings.json 中的 WeCom 凭据。
| Setting Key | 说明 |
|---|---|
WeCom.Enabled |
是否启用企业微信模块(默认 false) |
WeCom.CorpId |
企业 ID(企业微信管理后台获取) |
WeCom.CorpSecret |
应用 Secret(加密存储) |
WeCom.AgentId |
应用 AgentId |
WeCom.CallbackToken |
回调 Token(加密存储) |
WeCom.CallbackEncodingAesKey |
回调消息 AES 加解密 Key(加密存储) |
WeCom.HttpTimeoutMs |
HTTP 请求超时毫秒数(默认 30000) |
WeCom.TokenRefreshAdvanceSeconds |
Access Token 提前刷新秒数(默认 300) |
3. 配置企业微信回调 URL
在企业微信管理后台,将回调 URL 配置为:
https://your-domain/api/wecom/callback
回调 URL 必须携带 ABP 租户解析参数,例如:
https://your-domain/api/wecom/callback?__tenant=your-tenant
模块自动处理 URL 验证(GET)和事件接收(POST),通过 Token + AES 签名保证安全。缺少有效 租户上下文时,回调会被拒绝。
4. 从非多租户版本升级
2.0.0 是破坏性升级。生成并执行宿主 EF Core migration 前必须:
- 排空或显式清理旧版已入队的 WeCom 后台任务;旧 payload 没有
TenantId,升级后不会按 Host 任务继续执行。 - 为旧 WeCom 表中
TenantId IS NULL的记录选择明确策略:回填到指定租户、删除,或中止 迁移等待人工处理。禁止静默回填为Guid.Empty。 - 删除旧
TenantKey列(若曾部署过包含该列的预览版本),将所有 WeCom 表的TenantId改为NOT NULL,并重新创建以TenantId开头的业务唯一索引。 - 更新所有直接构造 WeCom 领域实体的宿主代码,为公开构造函数传入非空
tenantId。
API 参考
回调接口(匿名访问)
# URL 验证(企业微信回调配置时调用)
GET /api/wecom/callback?__tenant=your-tenant&msg_signature=xxx×tamp=xxx&nonce=xxx&echostr=xxx
# 事件接收(外部联系人/客户群变更回调)
POST /api/wecom/callback?__tenant=your-tenant&msg_signature=xxx×tamp=xxx&nonce=xxx
同步管理
# 触发全量同步外部联系人
POST /api/wecom/sync/contacts
# 触发全量同步客户群
POST /api/wecom/sync/group-chats
# 查询同步任务详情
GET /api/wecom/sync/tasks/{id}
# 查询同步任务列表
GET /api/wecom/sync/tasks
# 重试失败的回调事件
POST /api/wecom/sync/retry-failed-callbacks
外部联系人
# 分页查询外部联系人列表
GET /api/wecom/external-contacts
# 根据 ID 获取外部联系人详情
GET /api/wecom/external-contacts/{id}
# 根据 ExternalUserId 获取外部联系人详情
GET /api/wecom/external-contacts/by-external-user-id/{externalUserId}
# 获取外部联系人的跟进成员列表
GET /api/wecom/external-contacts/{id}/follow-users
# 分页查询外部联系人变更日志
GET /api/wecom/external-contacts/change-logs
客户群
# 分页查询客户群列表
GET /api/wecom/external-group-chats
# 根据 ID 获取客户群详情
GET /api/wecom/external-group-chats/{id}
# 根据 ChatId 获取客户群详情
GET /api/wecom/external-group-chats/by-chat-id/{chatId}
# 获取客户群成员列表
GET /api/wecom/external-group-chats/{id}/members
# 分页查询客户群变更日志
GET /api/wecom/external-group-chats/change-logs
客户联系 / 消息推送
所有接口要求有效租户上下文与 WeCom.MassMessages 权限。创建、提醒、停止和同步另需
对应的 .Create、.Remind、.Stop、.Sync 子权限,Host 侧不能调用。
POST /api/wecom/mass-messages # 创建企业群发
POST /api/wecom/mass-messages/{id}/remind # 提醒成员群发
POST /api/wecom/mass-messages/{id}/stop # 停止企业群发
GET /api/wecom/mass-messages # 分页查询数据库记录
GET /api/wecom/mass-messages/{id} # 查询数据库记录详情
GET /api/wecom/mass-messages/{id}/tasks # 分页查询成员发送任务
GET /api/wecom/mass-messages/{id}/results # 分页查询接收者发送详情
POST /api/wecom/mass-messages/sync # 立即刷新本模块创建的未完成群发
id 是模块返回的本地 GUID,messageId 是企业微信的 msgid,定时群发在实际创建前为 null。
列表查询支持 messageId、chatType、status、startTime、endTime、skipCount 和 maxResultCount。
列表时间筛选使用 Unix 秒;发送详情支持 userId、status 和分页参数。所有 GET 接口直接
查询数据库,不访问企业微信,因此同步完成后的记录可长期查询。列表只包含通过本模块创建的
群发,不包含企业微信管理后台或成员个人创建的群发。
创建请求沿用企业微信的字段名称,例如发送给客户:
{
"chat_type": "single",
"sender": "member-userid",
"external_userid": ["customer-external-userid"],
"text": { "content": "客户通知内容" },
"attachments": [
{ "msgtype": "image", "image": { "media_id": "uploaded-image-media-id" } }
]
}
发送给客户群时设 chat_type=group,必须提供 sender,可通过 chat_id_list 指定客户群。
支持 tag_filter.group_list[].tag_list 筛选客户;标签组内为或关系,组间为且关系。
文字最多 4000 个 UTF-8 字节,附件最多 9 个,支持 image、link、miniprogram、video、file。
附件内容使用对应的企业微信原始 JSON 字段;图片可先通过下述上传 API 获取 media_id。创建结果中的
failListJson 保存无法发送的客户或群 ID,requestJson 保存原始创建请求,contentJson
保存最新消息快照。
创建接口只创建任务并通知成员,成员确认后才发送给客户或客户群。提醒频率限制由
企业微信校验(24 小时内每个群发最多三次);停止只阻止继续发送,不撤回已发送的消息。
企业微信返回失败时,不更新本地提醒时间或停止时间。外部调用与数据库提交不能组成
原子事务:如果企业微信已经成功但本地提交失败,本地不会留下该群发的 msgid,同步也不会
从企业微信导入它,需要人工在企业微信后台核对;不要自动重试创建而生成重复的外部任务。
定时同步与执行状态
同步只针对本模块创建并已保存到本地的群发,不调用群发记录列表接口,也不导入其他群发。
默认 ABP Background Worker 每 5 分钟扫描存在未完成群发的租户,逐条通过 msgid 查询成员
任务和接收者发送结果,两层接口的游标分页都会完整读取。数据保存在数据库中,服务重启后
仍能继续;没有未完成群发的租户不会访问企业微信。
群发达到完成状态后不再刷新;停止后还会刷新一次最终结果。按租户、msgid 去重记录,
按记录、成员、接收者去重发送详情。任务状态为
0 未发送、2 已发送;接收者状态为 0 未发送、1 已发送、2 客户不是好友、3 客户已收到其他
群发。客户群未选择目标时,官方不会返回未发送的接收者记录。resultsComplete 仅在至少
存在一项成员任务、全部成员已发送且已知接收者都处于终态时成立。
每条群发先读完全部分页,再一次性写入任务和结果;调用失败的群发不写入本次数据,保持
未完成状态,后续周期重新同步,同租户的其他群发照常处理,全部处理后再返回首个失败。
每个租户有独立作用域、工作单元和同步锁,一个租户失败不阻断其他租户。同步时间由 ABP
IClock 提供;宿主停止时 Worker 会取消正在进行的同步。
宿主可配置周期,或在只承担 HTTP 请求的进程中关闭 Worker:
Configure<WeComMassMessageSyncOptions>(options =>
{
options.IsEnabled = true;
options.PeriodMilliseconds = 300000;
});
默认调度从共享 WeCom 数据库中读取存在未完成群发的租户 ID,仅在此步骤临时关闭租户过滤器;
群发数据与凭据访问始终在恢复后的租户作用域内进行。独立数据库租户需要宿主在对应的
租户作用域中调度 WeComMassMessageManager.SyncAsync()。同步锁依赖 ABP Distributed
Locking 抽象,默认使用进程内实现;多实例宿主应配置共享锁 Provider(如 Redis)并加载
AbpDistributedLockingModule。企业微信应用必须具有客户联系消息推送权限及相应成员
可见范围,模块使用现有租户凭据和 Token 服务。
数据库接入与升级
新增三张表:WeComMassMessages、WeComMassMessageTasks、WeComMassMessageResults
(实际前缀由 WeComDbProperties 决定)。所有表的 TenantId 非空,业务唯一索引以租户开头,
子表通过群发记录关联并级联删除。
从早期版本升级的宿主需要生成一次迁移:删除 WeComMassMessageSyncStates 表,以及
WeComMassMessages 的 Creator、CreateType 列。旧版本从企业微信列表导入的、非本模块创建的
群发记录仍留在 WeComMassMessages 中;如需清理,宿主应按 RequestJson IS NULL 识别后自行删除,
否则其中未完成的记录会继续被同步。
直接使用 WeComDbContext 的宿主会获得新模型;实现 IWeComDbContext 并使用
ReplaceDbContext 的宿主需补充以下 DbSet,继续调用 builder.ConfigureWeCom():
public DbSet<WeComMassMessage> MassMessages { get; set; } = null!;
public DbSet<WeComMassMessageTask> MassMessageTasks { get; set; } = null!;
public DbSet<WeComMassMessageResult> MassMessageResults { get; set; } = null!;
这些类型位于 Tai.WeCom.MassMessaging。数据库驱动、迁移和 ModelSnapshot 属于实际
消费宿主,本模块不绑定数据库驱动或发布宿主迁移。部署前必须在宿主的迁移项目生成
并应用 migration,确保三张表存在,再启动 Worker,例如:
dotnet ef migrations add AddWeComMassMessaging --project <host-migrations-project> --startup-project <host-project>
dotnet ef database update --project <host-migrations-project> --startup-project <host-project>
官方协议参考:创建企业群发、 提醒成员群发、 停止企业群发、 群发记录、任务和执行结果。
定时发送与截止日期
创建群发和创建朋友圈都支持两个可选字段,单位为 Unix 秒:
| 字段 | 含义 |
|---|---|
scheduled_time |
预定发送时间。早于或等于当前时间时立即创建;未到时间时只保存本地记录,状态为 Scheduled |
expire_time |
截止时间,必须晚于当前时间及 scheduled_time。到期后自动停止,已发送内容不撤回 |
{
"chat_type": "single",
"sender": "member-userid",
"text": { "content": "活动通知" },
"scheduled_time": 1800000000,
"expire_time": 1800086400
}
这两个字段只在本模块使用,不转发给企业微信;requestJson 保存的也是不含这两个字段的原始请求。
WeComPublishScheduleWorker 默认每 60 秒按租户扫描到期项,因此实际发送和停止最多延迟一个周期。
扫描逐条在独立工作单元中处理:调用企业微信前先将状态保存为 Dispatching,成功后改为
Created(朋友圈为 Creating),企业微信返回业务错误时改为 Failed,并记录 errorMessage。
进程在调用期间崩溃时,记录会停留在 Dispatching 并且不会自动重试,以免重复群发,需要人工到企业微信后台核对。
如果处理时已经超过截止时间,记录直接取消,不再发送。
尚未发送的定时记录调用停止接口时只在本地取消,状态为 Canceled。已创建的记录调用企业微信停止接口。
图片、视频等 media_id 只有三天有效期,预约发送时间应留在有效期内,或使用长期有效的图片 URL。
| 状态 | 群发 WeComMassMessageStatus |
朋友圈 WeComMomentStatus |
|---|---|---|
| 待发送 | Scheduled = 1 |
Scheduled = 0 |
| 调用中 | Dispatching = 2 |
Dispatching = 1 |
| 等待企业微信异步结果 | — | Creating = 2 |
| 已创建 | Created = 0(兼容旧数据) |
Created = 3 |
| 失败 | Failed = 3 |
Failed = 4 |
| 本地取消 | Canceled = 4 |
Canceled = 5 |
在只承担 HTTP 的进程中可关闭扫描,或调整周期:
Configure<WeComPublishScheduleOptions>(options =>
{
options.IsEnabled = true;
options.PeriodMilliseconds = 60000;
});
同一租户的群发调度和同步共用一把分布式锁,朋友圈调度和同步共用另一把。多实例部署必须配置共享锁 Provider。
独立数据库租户需要宿主在租户作用域中调用 WeComMassMessageManager.ProcessScheduleAsync() 和
WeComMomentManager.ProcessScheduleAsync()。
客户朋友圈
所有接口要求租户上下文与 WeCom.Moments 权限,写操作还需要 .Create、.Stop、.Sync 子权限。
POST /api/wecom/moments # 创建发表任务(支持定时与截止)
POST /api/wecom/moments/{id}/stop # 停止发表
GET /api/wecom/moments # 分页查询发表记录
GET /api/wecom/moments/{id} # 查询记录详情
GET /api/wecom/moments/{id}/tasks # 成员发表情况
GET /api/wecom/moments/{id}/results # 成员发表后的可见客户
POST /api/wecom/moments/sync # 立即解析创建结果并刷新未完成记录
POST /api/wecom/media/moment-attachments # 上传朋友圈图片/视频附件
创建请求使用创建发表任务的字段:
{
"text": { "content": "新品上市" },
"attachments": [
{ "msgtype": "link", "link": { "title": "详情", "url": "https://example.com", "media_id": "image-media-id" } }
],
"visible_range": {
"sender_list": { "user_list": ["zhangsan"], "department_list": [2] },
"external_contact_list": { "tag_list": ["etXXXX"] }
}
}
text 和 attachments 至少提供一项,文本最多 4000 字节。附件只能是同一种类型:最多 9 张图片、
1 个视频或 1 个链接。链接的 URL 必须为 http(s),标题最多 128 字节,media_id 必须是图片附件。
sender_list 为空时由企业微信按可见范围处理;tag_list 用于按客户标签筛选可见客户。
附件上传接口接收 multipart 字段 file 和 type(image 或 video),需要 WeCom.Moments.UploadAttachment
权限。它调用 upload_attachment(attachment_type=1),返回的 mediaId 三天有效,只能用于朋友圈附件。
企业微信异步创建朋友圈:模块保存返回的 jobid,状态为 Creating;WeComMomentSyncWorker 默认每 5 分钟
查询任务结果并保存 momentId。jobid 24 小时后失效,届时仍没有结果的记录标记为 Failed。
停止仍在 Creating 的记录时,模块先尝试获取结果;仍未完成时返回 WeCom:MomentNotReady(409),可以稍后重试。
已创建的记录会同步成员发表任务(publishStatus 0 未发表、1 已发表),并读取已发表成员的可见客户。
所有成员都已发表后记录完成,不再刷新;停止后还会刷新一次最终结果。列表支持 momentId、status、
startTime、endTime 和分页,可见客户支持 userId 筛选。和群发一样,模块只同步本模块创建的朋友圈,
外部调用与本地提交之间没有原子事务。
Configure<WeComMomentSyncOptions>(options =>
{
options.IsEnabled = true;
options.PeriodMilliseconds = 300000;
});
官方协议参考:客户朋友圈。
客户朋友圈规则组
规则组接口直接调用企业微信,不保存本地数据。读取需要 WeCom.MomentStrategies,写操作需要
.Create、.Update、.Delete 子权限。
GET /api/wecom/moment-strategies?cursor=&limit=1000 # 规则组 ID 列表
GET /api/wecom/moment-strategies/{strategyId} # 规则组详情
GET /api/wecom/moment-strategies/{strategyId}/range # 管理范围(cursor、limit 分页)
POST /api/wecom/moment-strategies # 创建,返回 strategyId
PUT /api/wecom/moment-strategies/{strategyId} # 编辑名称、管理员、权限,并增删管理范围
DELETE /api/wecom/moment-strategies/{strategyId}
管理员最多 20 人,每次范围节点最多 3000 个。范围节点 type=1 时传 userId,type=2 时传 partyId。
企业微信要求创建、编辑规则组串行执行,模块对同一租户加锁,最多等待 30 秒;超时返回 WeCom:OperationInProgress(409)。
协议参考:客户朋友圈规则组管理。
企业客户标签
标签接口同样直接调用企业微信。读取需要 WeCom.CorpTags,写操作需要 .Create、.Update、.Delete 子权限。
GET /api/wecom/corp-tags?tagIds=..&groupIds=.. # 查询标签库,不传参数时返回全部
POST /api/wecom/corp-tags # 添加标签;传 groupId 加入已有组,或传 groupName 新建组
PUT /api/wecom/corp-tags/{id} # 编辑标签或标签组的名称、排序
DELETE /api/wecom/corp-tags?tagIds=..&groupIds=.. # 删除标签或标签组
名称最多 30 个字符,order 取值范围为 0 到 2^32-1。删除标签组中的全部标签时,企业微信会同时删除该组。
返回的标签 ID 可用于朋友圈 tag_list 和群发 tag_filter。
协议参考:管理企业标签。
朋友圈与定时发送升级迁移
宿主需要生成一次 EF Core 迁移,例如 dotnet ef migrations add AddWeComMomentsAndSchedule,并确认包含:
WeComMassMessages:MessageId改为可空;新增Status(默认 0,即已有记录为Created)、ScheduledTime、ExpireTime、ErrorMessage(最大 2048)列。- 新表
WeComMoments、WeComMomentTasks、WeComMomentResults,子表级联删除。
WeComMassMessages 和 WeComMoments 使用相同的索引:
| 索引 | 用途 |
|---|---|
(TenantId, MessageId) / (TenantId, MomentId) 唯一 |
防止重复记录,并按企业微信 ID 查询 |
(TenantId, WeComCreateTime) |
租户内的列表查询和排序 |
(Status, ScheduledTime) |
调度 Worker 查找到期的定时记录 |
(Status, ResultsComplete, StoppedTime, ExpireTime) |
查找到截止时间、待停止的记录,以及同步时查找未完成的记录 |
后两个索引以状态开头,因为 Worker 先跨租户查找到期记录,再进入各租户处理。这样只需定位少量未完成的记录,
不用每个周期扫描全部历史记录。升级时会删除旧的 WeComMassMessages(TenantId, ResultsComplete) 索引,
它的用途已由最后一个索引覆盖。
朋友圈子表的唯一索引是 (MomentRecordId, UserId) 和 (MomentRecordId, UserId, ExternalUserId)。
父记录 ID 已经确定了租户,不需要再加 TenantId。这个唯一索引同时用于外键查找和级联删除,
EF 不会再另建一个外键索引。
实现 IWeComDbContext 的宿主需要补充以下 DbSet(类型位于 Tai.WeCom.Moments):
public DbSet<WeComMoment> Moments { get; set; } = null!;
public DbSet<WeComMomentTask> MomentTasks { get; set; } = null!;
public DbSet<WeComMomentResult> MomentResults { get; set; } = null!;
源码兼容性变化:WeComMassMessageDto.MessageId 改为 string?;IWeComApiClient 新增
UploadAttachmentAsync,自定义实现需要适配;WeComMassMessageManager 改为继承
WeComPublishManagerBase,构造函数不变。WeCom:InvalidPublishState、WeCom:MomentNotReady、
WeCom:OperationInProgress 映射为 HTTP 409。应用迁移后再启动调度和同步 Worker。
数据模型
同步任务生命周期
Pending → Running → Completed
→ Failed
全量同步通过 ABP 后台作业执行,适合大量数据的分页分批处理。每个同步任务记录已处理数、错误数和总数,便于追踪进度。
回调事件处理
收到事件 → Pending → Processing → Completed
→ Failed
回调事件首先落库(5 秒内返回 success),然后通过后台作业异步处理。处理失败的记录可手动重试。
变更日志
每次外部联系人或客户群数据变更(无论是通过 API 回调还是全量同步)都会写入变更日志,记录变更前后的数据快照、变更摘要、变更来源(Callback / FullSync)。
回调事件类型
| 事件类型 | ChangeType | 说明 |
|---|---|---|
change_external_contact |
add_external_contact |
添加外部联系人 |
add_half_external_contact |
添加未互发消息的外部联系人 | |
edit_external_contact |
编辑外部联系人 | |
del_external_contact |
企业成员删除外部联系人 | |
del_follow_user |
外部联系人删除企业成员 | |
transfer_fail |
客户接替失败 | |
change_external_chat |
create |
创建客户群 |
update |
更新客户群信息 | |
dismiss |
解散客户群 |
技术栈
- .NET 10 / ABP 10.6.1
- 企业微信服务端 API(外部联系人、客户群、回调)
- EF Core(数据持久化与仓储)
- ABP Distributed Cache(Access Token 缓存)
- ABP Background Jobs(长耗时全量同步异步执行)
- AES 加解密(回调消息安全)
消费宿主需使用 .NET 10 SDK,并将 ABP 包统一升级到 10.6.1。现有 AutoMapper 映射方式保持不变;Volo.Abp.AutoMapper 10.6.1 仍依赖 AutoMapper 14.0.0,NuGet 会报告 GHSA-rvv3-g6hj-g44x。升级 ABP 本身不会消除该警告,未将其屏蔽。
Nuget
dotnet pack Tai.WeCom.slnx --configuration Release --output nupkgs
dotnet nuget push "nupkgs/*.nupkg" --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate
License
MIT
按客户添加时间查询
GET /api/wecom/external-contacts 新增以下可选查询参数(Unix 秒,区间为左闭右开):
| 参数 | 含义 |
|---|---|
addedStartTime / addedEndTime |
当前保存的成员关系的添加时间;任一关系满足整个区间即可,包含已标记删除的关系 |
firstAddedStartTime / firstAddedEndTime |
企业内已知首次添加时间 |
两组条件同时传入时取交集;在数据库筛选后统计、分页,每个客户只返回一次。时间上界必须大于
下界,单边筛选也受支持。列表和详情均返回 firstAddedTime,列表中的 externalUserId 可以用于
POST /api/wecom/mass-messages 的 external_userid。
GET /api/wecom/external-contacts?firstAddedStartTime=1700000000&firstAddedEndTime=1700086400&skipCount=0&maxResultCount=100
FirstAddedTime 是可空的 Unix 秒字段,保存所有已知成员关系的最早添加时间。全量同步和客户
回调都会更新它,只允许向更早更新;删除关系和重新添加不会使它变晚。成员关系自己的
WeComCreateTime 则按最新快照更新,因此可以查询重新添加的时间。首次时间未知的客户不会
匹配首次添加时间筛选。此前未保存且企业微信不再返回的历史关系无法还原;该字段代表已知最早时间。
首次添加时间升级迁移
本仓库没有 wecom 数据库宿主、数据库驱动或迁移基线,迁移和 ModelSnapshot 必须在实际消费 宿主生成。在宿主的 EF Core 迁移项目执行:
dotnet ef migrations add AddWeComExternalContactFirstAddedTime
确认生成的迁移和 ModelSnapshot 包含:
ExternalContacts.FirstAddedTime可空long列,不设置默认值。ExternalContacts(TenantId, FirstAddedTime)索引。ContactFollowUsers(TenantId, WeComCreateTime, ExternalContactId)索引。
实际表前缀和 Schema 以宿主配置为准。在部署新版应用和开启同步前应用迁移;租户独立数据库
需要逐库升级。本次不自动回填历史数据,由消费方自行处理。历史客户首次时间初始为 null,
后续同步会逐步记录已知最早时间。首次时间筛选不会返回尚未回填或同步的未知客户。
Down 使用生成的删除索引和列操作;回滚会丢失已积累的首次时间,回滚前应保留数据备份。
模块测试使用 SQLite 验证模型、筛选和同步,不能代替实际宿主的 Schema 迁移验证。
上传群发图片
POST /api/wecom/media/images 接收 multipart/form-data,文件字段名为 file。要求租户上下文,
并具备 WeCom.MassMessages 和 WeCom.MassMessages.UploadImage 权限。
curl -X POST 'https://your-host/api/wecom/media/images' \
-H 'Authorization: Bearer <access-token>' \
-H '__tenant: <tenant-id>' \
-F 'file=@image.png'
响应示例(时间为 Unix 秒):
{
"mediaId": "uploaded-image-media-id",
"createdAt": 1700000000,
"expiresAt": 1700259200
}
将 mediaId 放入创建群发的图片附件:
{
"msgtype": "image",
"image": { "media_id": "uploaded-image-media-id" }
}
按企业微信上传临时素材文档,
图片支持 JPG/JPEG、PNG,大小必须大于 5 字节且不超过 10MB,media_id 有效期为三天。
本接口检查扩展名、文件头和大小,再以 media 字段转发给企业微信;最终图片有效性由企业微信
校验。创建时间来自企业微信响应,过期时间为创建时间加三天,过期后需要重新上传。
上传只转发到企业微信,不保存图片文件或新增数据库记录,因此不需要数据库迁移。宿主或反向
代理的请求体限制需要允许 10MB 图片及 multipart 开销;模块接口限制为 10MB + 64KB。
进程内调用 IWeComMediaAppService.UploadImageAsync(UploadWeComImageDto) 使用文件名及字节数组;
该进程内入口不生成 JSON 远程接口,外部客户端使用上面的显式 multipart API。
自定义 IWeComApiClient 实现需要适配新增 UploadImageAsync 方法。
素材管理的 REST API
素材使用独立权限 WeCom.Media;同步上传还需 WeCom.Media.Upload,创建异步任务还需
WeCom.Media.CreateUploadJob。所有调用都要求租户上下文,使用该租户的企业微信凭据。
已有 /api/wecom/media/images 保留原有群发权限和响应行为,新调用建议使用以下资源接口。
| 方法和路径 | 输入 | 成功响应 |
|---|---|---|
POST /api/wecom/media |
multipart:file、type |
201;type、mediaId、createdAt、expiresAt |
POST /api/wecom/media/image-urls |
multipart:file |
201;url,Location 指向返回的图片 URL |
POST /api/wecom/media/jobs |
JSON:scene、type、fileName、url、md5 |
202;jobId、status,Location 指向任务查询 API |
GET /api/wecom/media/jobs/{jobId} |
任务 ID | 200;任务状态和可用结果/失败详情 |
资源名称使用复数名词,创建使用 POST,查询使用 GET。按
HTTP Semantics 区分同步创建和异步受理,
不将 202 当成上传成功。任务失败是资源状态,查询仍返回 200,status=failed 并带
errorCode、errorMessage;接口调用本身失败则使用 ABP 错误响应。本模块将 WeCom:ApiCallFailed
映射为 502、WeCom:TenantRequired 映射为 400,这些映射也适用于模块其他接口。
参数校验使用 400,认证/授权错误使用 401/403;超过 HTTP 请求体限制由宿主返回 413。
同步临时素材
依据上传临时素材:
type |
限制 |
|---|---|
image |
大于 5 字节、不超过 10MB;JPG/JPEG、PNG |
voice |
大于 5 字节、不超过 2MB;AMR,不超过 60 秒 |
video |
大于 5 字节、不超过 10MB;MP4 |
file |
大于 5 字节、不超过 20MB |
模块校验大小、扩展名和图片文件头;语音时长、媒体完整性等由企业微信校验。type 默认为
image。media_id 从企业微信创建时间起三天有效,过期需重新上传。语音上传能力不代表客户群发
支持语音附件;群发附件类型仍以群发接口协议为准。
curl -X POST 'https://your-host/api/wecom/media' \
-H 'Authorization: Bearer <access-token>' -H '__tenant: <tenant-id>' \
-F 'type=file' -F 'file=@document.pdf'
永久图片
企业微信当前上传图片接口返回长期有效
的图片 URL,不返回永久 media_id;它与微信公众号的永久素材接口不同。图片不超过 2MB,
返回 URL 用于企业微信允许的图片展示场景,不能作为通用外部站点图片 CDN。官方限制为每企业
每天最多 1000 张、每月最多 3000 张,由企业微信校验。
curl -X POST 'https://your-host/api/wecom/media/image-urls' \
-H 'Authorization: Bearer <access-token>' -H '__tenant: <tenant-id>' \
-F 'file=@image.png'
创建企业群发的图片附件可以使用此
接口返回的 url 填入 image.pic_url;不能使用任意外部图片 URL 代替。
异步上传任务
依据异步上传临时素材,目前仅支持
scene=1(客户联系入群欢迎语素材)、type=video|file,大小大于 5 字节且不超过 200MB。
视频要求 MP4,图片和语音不支持异步上传。CDN URL 必须支持 Range 分块下载,下载和 MD5、大小
检查由企业微信执行;本模块不访问或下载 URL。腾讯云 COS 链接需要公有读权限。
文件名上限 128 UTF-8 字节、URL 上限 1024 UTF-8 字节,MD5 为 32 位十六进制字符串。
jobId 上限 128 UTF-8 字节,官方查询有效期为 60 分钟,调用方需要及时查询并保存完成结果。
本模块采用主动查询方式,不保存任务或定时刷新,也不自动处理 upload_media_job_finish 回调。
POST /api/wecom/media/jobs
Content-Type: application/json
{
"scene": 1,
"type": "video",
"fileName": "large.mp4",
"url": "https://your-cdn/large.mp4",
"md5": "00000000000000000000000000000000"
}
202 响应的 Location 可用于 GET 查询,status 为 processing、completed 或 failed。
仅完成状态返回 mediaId、createdAt、expiresAt;失败状态返回企业微信的任务错误码和消息。
异步产出的 media_id 同样三天有效,但使用范围与普通临时素材不同,不能直接用于企业群发。
三种模式都不新增数据库记录或保存本地文件,无新增迁移。宿主上传限制需要允许最大 20MB
同步文件及 multipart 开销。上传和任务查询为显式 HTTP API;进程内入口仍为
IWeComMediaAppService,新增接口方法及 IWeComApiClient 的同步素材方法需要自定义实现适配。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Tai.WeCom.Domain (>= 2.2.0)
- Volo.Abp.EntityFrameworkCore (>= 10.7.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Tai.WeCom.EntityFrameworkCore:
| Package | Downloads |
|---|---|
|
Tai.WeCom.Installer
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.