feat: add workbench and client settings APIs
This commit is contained in:
+106
-6
@@ -1,7 +1,7 @@
|
||||
# IM 第一至六章后续增强项
|
||||
# IM 第一至九章后续增强项
|
||||
|
||||
> 日期:2026-08-13
|
||||
> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章群消息与 @、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理
|
||||
> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章群消息与 @、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构
|
||||
> 用途:记录当前已完成基线之外的增强项,后续统一排期处理。
|
||||
|
||||
## 1. 第一章:登录、注册与安全
|
||||
@@ -167,9 +167,109 @@
|
||||
| P1 | 群解散与二维码吊销 | 群解散、二维码撤销、邀请失效需要状态联动和权限回收。 | 解散后 Topic、二维码、邀请、文件新增权限全部失效。 |
|
||||
| P2 | 大群性能与稳定分页 | 成员、申请、公告、禁言、文件列表都需要稳定游标和大群压测。 | 大群场景下分页不重复不漏,接口延迟达标。 |
|
||||
|
||||
## 7. 统一处理建议
|
||||
## 7. 第七章:文件下载、收藏管理与聊天记录检索
|
||||
|
||||
1. 先做 P0:统一幂等、实时事件/Outbox、验证码真实发送与限频、P2P/好友/搜索隐私策略、第六章群业务独立表与事务状态机。
|
||||
2. 再做 P1:租户策略配置、系统通知生产通道、实名审核、设备管理、资料隐私裁剪、关系审计、群禁言发送校验和群文件索引。
|
||||
3. 最后做 P2:端到端重连测试、schema 版本演进、大群压测、批量接口体验优化。
|
||||
### 7.1 当前已完成基线
|
||||
|
||||
1. 收藏创建、列表、详情和删除接口已具备,支持文本、文件、语音、链接等类型。
|
||||
2. 收藏批量创建和基础分享前的数据结构已具备。
|
||||
3. Topic 消息关键词搜索、文件/图片/视频等媒体类型检索接口已具备。
|
||||
4. 消息上下文定位接口可围绕指定 seq 返回前后消息。
|
||||
5. 文件访问已接入用户与 Topic 权限校验,下载前可判断资源可见性。
|
||||
|
||||
### 7.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 收藏幂等与去重策略 | 收藏同一消息、同一文件或同一链接需要统一去重和幂等键处理。 | 重复点击收藏不会产生重复记录;超时重试返回同一收藏 ID。 |
|
||||
| P0 | 收藏分享权限二次校验 | 收藏文件或消息分享给好友/群时,需要重新校验接收方是否可访问原资源或生成受控副本。 | 无权限接收方不能通过收藏分享拿到原会话附件。 |
|
||||
| P0 | 文件下载短时签名 URL | 当前权限判断已具备,后续应生成短时下载 URL,并记录下载审计。 | URL 过期不可访问;退群/拉黑后旧 URL 失效或受策略限制。 |
|
||||
| P0 | 文件安全扫描与 MIME 校验 | 上传后的文件需要扩展名、MIME、内容嗅探、病毒扫描和图片/视频解码安全检查。 | 高风险文件不可下载或转发,客户端收到稳定错误码。 |
|
||||
| P1 | 收藏分类与全文索引 | 收藏列表需要按类型、来源、时间、关键词、发送人等维度检索。 | 大量收藏下搜索响应稳定,类型筛选结果准确。 |
|
||||
| P1 | 聊天记录稳定游标 | 消息搜索和媒体列表需要稳定游标,避免新消息插入导致翻页重复或漏项。 | 搜索期间新增消息不影响当前翻页结果。 |
|
||||
| P1 | 历史消息权限快照策略 | 用户退群、删除好友、拉黑后,历史消息和附件的可见范围需要统一规则。 | 各入口对同一历史资源权限判断一致。 |
|
||||
| P1 | 下载状态与断点续传 | 大文件下载需要断点续传、失败重试、进度状态和下载完成事件。 | 网络中断后可续传;下载完成状态可跨端同步或按端隔离。 |
|
||||
| P2 | 聊天记录高亮与命中片段 | 搜索结果可返回命中摘要、高亮范围和消息类型摘要。 | 客户端无需二次解析即可展示命中片段。 |
|
||||
| P2 | 收藏导出与批量管理 | 后续可支持收藏批量删除、批量转发、导出和云端容量统计。 | 批量操作有部分失败语义和可重试结果。 |
|
||||
|
||||
## 8. 第八章:实时语音与视频通话
|
||||
|
||||
### 8.1 当前已完成基线
|
||||
|
||||
1. 通话 invite、accept、offer、answer、ice-candidate、hang-up 等事件沿用 Tinode call event 链路。
|
||||
2. 通话记录表 `im_call_records` 已具备,记录 `call_id`、Topic、seq、主叫/被叫、类型、状态、时长和结束原因。
|
||||
3. 通话记录创建、接听和结束状态更新已接入 store/adapter。
|
||||
4. 通话记录查询接口已具备:全局 `GET /v1/im/calls` 与 Topic 内 `GET /v1/im/topics/{topic}/calls`。
|
||||
5. 查询结果返回对端用户、方向、通话类型、状态、时长和关键时间点,客户端可先完成通话记录页对接。
|
||||
|
||||
### 8.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 跨设备抢答状态机 | 同一账号多端收到来电时,需要一个设备接听后自动取消其他端待接状态。 | A 手机接听后,A 平板和 Web 端立即变为已在其他设备接听。 |
|
||||
| P0 | 通话状态强一致与幂等 | invite、accept、hang-up 等事件需要幂等键或事件序号,避免乱序/重复导致状态回退。 | 重复 hang-up 不改变最终状态;迟到 accept 不会覆盖已结束通话。 |
|
||||
| P0 | 通话事件 Outbox 与通知 | 来电、未接、挂断、忙线等需要可信事件和 Push,DB 状态与通知通过 Outbox 衔接。 | 离线用户收到一次来电/未接提醒,失败可补偿。 |
|
||||
| P0 | TURN/STUN 配置与媒体准入 | 服务端应按租户/区域返回可用 ICE 配置,并限制未授权用户创建媒体会话。 | 客户端拉取到有效 TURN;无权限通话被服务端拒绝。 |
|
||||
| P1 | 结束原因标准化 | 需要统一 `missed`、`declined`、`busy`、`timeout`、`network_error` 等原因和客户端展示文案。 | 不同端同一结束原因展示一致,统计口径一致。 |
|
||||
| P1 | 通话质量指标 | 需要采集建立耗时、ICE 成功率、丢包、抖动、断连等指标。 | 后台可按租户和版本查看通话质量趋势。 |
|
||||
| P1 | 群通话/会议房间拆分 | 群音视频不宜复用 P2P call,需要会议房间、参与人状态和 SFU 接入。 | 群通话有独立 room_id、成员列表和退出/重进状态。 |
|
||||
| P1 | 通话权限统一策略 | 通话入口应复用 P2P/群关系策略、黑名单、禁言、青少年模式和企业策略。 | 被拉黑、非好友或受限制用户无法发起通话。 |
|
||||
| P2 | 通话记录聚合与清理 | 通话记录可按会话、日期、未接分类聚合,并支持保留周期清理。 | 记录列表分页稳定,超过保留期按策略清理。 |
|
||||
|
||||
## 9. 第九章:企业工作台与办公协同
|
||||
|
||||
### 9.1 当前已完成基线
|
||||
|
||||
1. 工作台接口 `GET /v1/im/workbench` 已具备,返回模块、布局版本和企业功能开关。
|
||||
2. 工作台模块表 `im_workbench_apps` 已具备,默认包含组织架构、会议通知、日报、月报、总经理信箱入口。
|
||||
3. 服务端对工作台 route 做内部白名单过滤,避免客户端执行任意服务端 URL。
|
||||
4. 组织架构部门表 `im_org_departments` 和成员表 `im_org_members` 已具备。
|
||||
5. 部门列表 `GET /v1/im/organization/departments`、部门成员 `GET /v1/im/organization/departments/{id}/members`、组织搜索 `GET /v1/im/organization/search` 已具备。
|
||||
6. 会议通知 `POST/GET/PATCH/CANCEL/REMIND/MY-RESPONSE` 已具备基线,包含参与人和版本冲突控制。
|
||||
7. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。
|
||||
8. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。
|
||||
|
||||
### 9.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 工作台权限与功能开关配置化 | 当前模块基线可返回,后续需要按租户、部门、角色、后台配置动态裁剪。 | 不同用户登录同一租户看到不同授权模块。 |
|
||||
| P0 | 工作台未读角标事件 | 文档要求 `workbench.badge.updated`,后续需要模块未读聚合和实时增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 |
|
||||
| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 |
|
||||
| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
|
||||
| P1 | 组织搜索索引优化 | 当前基础 LIKE 搜索可用,后续需要拼音、工号、手机号脱敏匹配和稳定排序。 | 大组织下搜索延迟达标,结果排序符合组织规则。 |
|
||||
| P1 | 组织版本与增量同步 | 部门和成员返回版本,后续需要全局组织版本、变更事件和客户端增量刷新。 | 切换部门或人员变更后客户端无需全量重拉。 |
|
||||
| P1 | 会议通知实时提醒 | 当前接口和表已具备,后续需要会议创建、更新、取消、响应后的在线事件和 Push。 | 参与人在线端无需刷新即可收到会议变化。 |
|
||||
| P1 | 日报/月报后台统计口径 | 当前基础统计可用,后续需要按部门、周期、迟交、补交、未读等维度扩展。 | 管理员看板与业务口径一致。 |
|
||||
| P1 | 总经理信箱后台流转 | 当前前台配置/提交已具备,后续需要后台处理、回复、转派、归档和通知。 | 信件全生命周期可追踪,匿名身份受保护。 |
|
||||
| P1 | 办公协同审计 | 组织查询、会议变更、报告查看、信箱提交等敏感动作需要审计。 | 后台可按租户、用户、对象查询办公操作流水。 |
|
||||
| P2 | 办公模块统一附件策略 | 会议、日报、月报、信箱附件需要复用第七章文件 ACL、用途和病毒扫描。 | 附件上传、查看、下载权限与业务状态一致。 |
|
||||
|
||||
## 10. 个人设置与客户端能力
|
||||
|
||||
### 10.1 当前已完成基线
|
||||
|
||||
1. 个人资料 `GET/PATCH /v1/im/me/profile`、个人页 `GET /v1/im/me` 已具备。
|
||||
2. 隐私设置 `GET/PATCH /v1/im/me/privacy` 已具备,支持版本冲突控制。
|
||||
3. 最新版本检查 `GET /v1/im/app/releases/latest` 已具备,支持未登录按租户解析调用。
|
||||
4. 青年模式查询、开启、关闭接口已具备,复用敏感操作 `action_token` 和独立密码校验链路。
|
||||
5. 设备 Push Token 覆盖写入 `PUT /v1/im/devices/{device_id}/push-token` 已具备。
|
||||
|
||||
### 10.2 后续增强项
|
||||
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 个人资料变更实时同步 | 资料、头像、隐私变更后需要向多端推送 `profile.updated`、`privacy.updated`。 | 同账号多端无需重登即可刷新设置。 |
|
||||
| P0 | 精准登出与会话撤销 | 现有 token 主要靠 `auth_version` 批量失效,后续需要 access/refresh/session 表支持当前设备登出和指定 session 删除。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 |
|
||||
| P0 | 青年模式策略配置化 | 当前返回默认 restrictions,后续需要后台按租户配置可用时段、内容限制、消费保护等规则。 | 不同租户返回不同限制策略且能热更新。 |
|
||||
| P0 | Push Token 生命周期 | 当前可覆盖写入,后续需要 token 失效回收、设备解绑、平台厂商错误码处理。 | 推送失败后自动禁用无效 token,重新登录可恢复。 |
|
||||
| P1 | 版本灰度发布 | 当前按 platform/channel 查最新版本,后续需要按租户、用户、灰度比例、最低版本强制升级。 | 灰度用户收到新版本,非灰度用户保持稳定版本。 |
|
||||
| P1 | 资料隐私裁剪落地 | 隐私设置需要接入用户搜索、组织架构、会话资料页等读路径。 | 非授权用户无法通过其他接口绕过隐私配置。 |
|
||||
| P2 | 客户端能力上报 | 后续增加设备能力、通知权限、厂商通道状态和前台活跃状态上报。 | 后台可定位推送不可达和版本兼容问题。 |
|
||||
|
||||
## 11. 统一处理建议
|
||||
|
||||
1. 先做 P0:统一幂等、实时事件/Outbox、验证码真实发送与限频、P2P/好友/搜索隐私策略、第六章群业务独立表与事务状态机、文件下载签名、通话强状态机、工作台/组织权限。
|
||||
2. 再做 P1:租户策略配置、系统通知生产通道、实名审核、设备管理、资料隐私裁剪、关系审计、群禁言发送校验、群文件索引、通话质量指标、会议/日报/月报/信箱完整业务表。
|
||||
3. 最后做 P2:端到端重连测试、schema 版本演进、大群压测、批量接口体验优化、收藏导出、通话记录聚合和办公模块附件统一策略。
|
||||
4. 每个增强项落地前同步更新 `IM-系统设计文档.md` 和 `IM-前后端对接文档.md` 中对应章节状态。
|
||||
|
||||
@@ -80,6 +80,237 @@ func main() {
|
||||
INDEX idx_im_call_records_callee(callee_id, id DESC),
|
||||
INDEX idx_im_call_records_status(status, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
route VARCHAR(128) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
unread_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_workbench_apps_code(code),
|
||||
INDEX idx_im_workbench_apps_enabled_order(enabled, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_org_departments(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_org_departments_department(department_id),
|
||||
INDEX idx_im_org_departments_parent(parent_id, state, sort_order, id),
|
||||
INDEX idx_im_org_departments_name(name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_org_members(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
display_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
position VARCHAR(128) NOT NULL DEFAULT '',
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
mobile VARCHAR(32) NOT NULL DEFAULT '',
|
||||
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
contactable TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_org_members_department_user(department_id, user_id),
|
||||
INDEX idx_im_org_members_department(department_id, employment_state, sort_order, id),
|
||||
INDEX idx_im_org_members_user(user_id),
|
||||
INDEX idx_im_org_members_display_name(display_name),
|
||||
CONSTRAINT fk_im_org_members_department FOREIGN KEY(department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_meetings(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
organizer_id BIGINT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
start_at DATETIME(3) NOT NULL,
|
||||
end_at DATETIME(3) NOT NULL,
|
||||
location VARCHAR(256) NOT NULL DEFAULT '',
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
|
||||
reminder_minutes JSON,
|
||||
idempotency_key VARCHAR(128),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
canceled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_meetings_meeting(meeting_id),
|
||||
UNIQUE INDEX uk_im_meetings_idempotency(organizer_id, idempotency_key),
|
||||
INDEX idx_im_meetings_organizer(organizer_id, start_at),
|
||||
INDEX idx_im_meetings_state_time(state, start_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
response VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
response_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_meeting_participants_user(meeting_id, user_id),
|
||||
INDEX idx_im_meeting_participants_user_time(user_id, meeting_id),
|
||||
INDEX idx_im_meeting_participants_response(meeting_id, response),
|
||||
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY(meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_daily_reports(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_date DATE NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_daily_reports_report(report_id),
|
||||
UNIQUE INDEX uk_im_daily_reports_author_date(author_id, report_date),
|
||||
INDEX idx_im_daily_reports_author_id(author_id, id DESC),
|
||||
INDEX idx_im_daily_reports_date_status(report_date, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_month CHAR(7) NOT NULL,
|
||||
work_summary TEXT NOT NULL,
|
||||
next_plan TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_report(report_id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_author_month(author_id, report_month),
|
||||
INDEX idx_im_monthly_reports_author_id(author_id, id DESC),
|
||||
INDEX idx_im_monthly_reports_month_status(report_month, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
allow_anonymous TINYINT NOT NULL DEFAULT 1,
|
||||
required_fields JSON,
|
||||
max_length INT NOT NULL DEFAULT 5000,
|
||||
attachment_policy JSON,
|
||||
notice_title VARCHAR(128) NOT NULL DEFAULT '',
|
||||
notice_content TEXT NOT NULL,
|
||||
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
sender_id BIGINT NOT NULL,
|
||||
subject VARCHAR(200) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
anonymous TINYINT NOT NULL DEFAULT 0,
|
||||
attachment_file_ids JSON,
|
||||
notice_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
submitted_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_submission(submission_id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_idempotency(sender_id, idempotency_key),
|
||||
INDEX idx_im_mailbox_submissions_sender(sender_id, id DESC),
|
||||
INDEX idx_im_mailbox_submissions_status(status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_user_profiles(
|
||||
user_id BIGINT NOT NULL,
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
birth_date DATE,
|
||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings(
|
||||
user_id BIGINT NOT NULL,
|
||||
add_me_methods JSON,
|
||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_app_releases(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
platform VARCHAR(32) NOT NULL,
|
||||
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||
latest_version VARCHAR(64) NOT NULL,
|
||||
min_supported_version VARCHAR(64) NOT NULL,
|
||||
update_type VARCHAR(16) NOT NULL DEFAULT 'none',
|
||||
release_notes TEXT NOT NULL,
|
||||
store_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
package_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
checksum VARCHAR(128) NOT NULL DEFAULT '',
|
||||
remind_after_seconds INT NOT NULL DEFAULT 0,
|
||||
published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_app_releases_platform_channel(platform, channel)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens(
|
||||
device_id VARCHAR(128) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
provider VARCHAR(32) NOT NULL,
|
||||
token VARCHAR(512) NOT NULL,
|
||||
app_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(device_id),
|
||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token),
|
||||
INDEX idx_im_device_push_tokens_user(user_id, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings(
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 0,
|
||||
policy_version INT NOT NULL DEFAULT 1,
|
||||
restrictions JSON,
|
||||
enabled_at DATETIME(3),
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(user_id, purpose)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err = db.Exec(stmt); err != nil {
|
||||
@@ -93,6 +324,10 @@ func main() {
|
||||
"ALTER TABLE tinode.im_tenant ADD COLUMN tenant_desc VARCHAR(256) AFTER name",
|
||||
"ALTER TABLE tinode.im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc",
|
||||
"ALTER TABLE tinode.im_tenant ADD COLUMN db_dsn VARCHAR(1024) NOT NULL DEFAULT '' AFTER db_name",
|
||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled TINYINT NOT NULL DEFAULT 0 AFTER password_hash",
|
||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN policy_version INT NOT NULL DEFAULT 1 AFTER enabled",
|
||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN restrictions JSON AFTER policy_version",
|
||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions",
|
||||
} {
|
||||
if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") {
|
||||
fatal(fmt.Errorf("exec failed: %s: %w", stmt, err))
|
||||
@@ -108,6 +343,28 @@ func main() {
|
||||
ON DUPLICATE KEY UPDATE updated_at=CURRENT_TIMESTAMP(3)`); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if _, err = db.Exec(`INSERT INTO tinode.im_workbench_apps(code,name,route,sort_order,unread_key,enabled,permission)
|
||||
VALUES
|
||||
('organization','组织架构','/organization',10,'organization',1,'enabled'),
|
||||
('meetings','会议通知','/meetings',20,'meetings',1,'enabled'),
|
||||
('daily_reports','日报','/reports/daily',30,'daily_reports',1,'enabled'),
|
||||
('monthly_reports','月报','/reports/monthly',40,'monthly_reports',1,'enabled'),
|
||||
('executive_mailbox','总经理信箱','/executive-mailbox',50,'executive_mailbox',1,'enabled')
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name),route=VALUES(route),sort_order=VALUES(sort_order),unread_key=VALUES(unread_key),updated_at=CURRENT_TIMESTAMP(3)`); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if _, err = db.Exec(`INSERT INTO tinode.im_mailbox_config(id,enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content)
|
||||
VALUES(1,1,1,'["subject","content"]',5000,'{"max_files":5,"purpose":"executive_mailbox"}','总经理信箱','请如实填写诉求,匿名提交将按企业政策保护展示身份。')
|
||||
ON DUPLICATE KEY UPDATE updated_at=updated_at`); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if _, err = db.Exec(`INSERT INTO tinode.im_app_releases(platform,channel,latest_version,min_supported_version,update_type,release_notes,remind_after_seconds)
|
||||
VALUES
|
||||
('ios','stable','1.0.0','1.0.0','none','当前已是最新版本。',0),
|
||||
('android','stable','1.0.0','1.0.0','none','当前已是最新版本。',0)
|
||||
ON DUPLICATE KEY UPDATE updated_at=CURRENT_TIMESTAMP(3)`); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if _, err = db.Exec("UPDATE tinode.im_tenant SET db_name='tinode' WHERE code='default' AND (db_name='' OR db_name IS NULL)"); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
@@ -292,6 +292,39 @@ type TenantBusinessAdapter interface {
|
||||
TenantCallCreate(t.TenantID, *t.CallRecord) error
|
||||
TenantCallAccept(t.TenantID, string, int, time.Time) error
|
||||
TenantCallFinish(t.TenantID, string, int, string, int64, time.Time) error
|
||||
TenantCallList(t.TenantID, t.Uid, string, string, int) ([]t.CallRecord, string, error)
|
||||
|
||||
TenantWorkbenchModules(t.TenantID, t.Uid) ([]t.WorkbenchModule, int, error)
|
||||
TenantOrganizationDepartments(t.TenantID, t.Uid, string, string, int) ([]t.OrganizationDepartment, string, error)
|
||||
TenantOrganizationMembers(t.TenantID, t.Uid, string, string, int) ([]t.OrganizationMember, string, error)
|
||||
TenantOrganizationSearch(t.TenantID, t.Uid, string, string, string, int) ([]t.OrganizationSearchItem, string, error)
|
||||
TenantMeetingCreate(t.TenantID, t.Uid, *t.Meeting, []t.Uid) (*t.Meeting, error)
|
||||
TenantMeetingGet(t.TenantID, t.Uid, string) (*t.Meeting, error)
|
||||
TenantMeetingUpdate(t.TenantID, t.Uid, string, t.MeetingPatch, string) (*t.Meeting, error)
|
||||
TenantMeetingCancel(t.TenantID, t.Uid, string, string) (*t.Meeting, error)
|
||||
TenantMeetingRemind(t.TenantID, t.Uid, string, string) ([]t.MeetingParticipant, error)
|
||||
TenantMeetingRespond(t.TenantID, t.Uid, string, string, int) (*t.Meeting, error)
|
||||
TenantDailyReportList(t.TenantID, t.Uid, string, string, string, string, string, int) ([]t.DailyReport, string, error)
|
||||
TenantDailyReportCreateDraft(t.TenantID, t.Uid, string, time.Time) (*t.DailyReport, error)
|
||||
TenantDailyReportGet(t.TenantID, t.Uid, string) (*t.DailyReport, error)
|
||||
TenantDailyReportSaveDraft(t.TenantID, t.Uid, string, string, []string, int) (*t.DailyReport, error)
|
||||
TenantDailyReportSubmit(t.TenantID, t.Uid, string, int) (*t.DailyReport, error)
|
||||
TenantDailyReportStats(t.TenantID, t.Uid, time.Time, string) (*t.DailyReportStats, error)
|
||||
TenantDailyReportRemind(t.TenantID, t.Uid, time.Time, []t.Uid, string) ([]t.Uid, error)
|
||||
TenantMonthlyReportList(t.TenantID, t.Uid, string, string, string, string, string, int) ([]t.MonthlyReport, string, error)
|
||||
TenantMonthlyReportCreateDraft(t.TenantID, t.Uid, string, string) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportGet(t.TenantID, t.Uid, string) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportSaveDraft(t.TenantID, t.Uid, string, string, string, []string, int) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportSubmit(t.TenantID, t.Uid, string, int) (*t.MonthlyReport, error)
|
||||
TenantMonthlyReportStats(t.TenantID, t.Uid, string, string) (*t.MonthlyReportStats, error)
|
||||
TenantMailboxConfigGet(t.TenantID, t.Uid) (*t.MailboxConfig, error)
|
||||
TenantMailboxSubmissionCreate(t.TenantID, t.Uid, *t.MailboxSubmission, string) (*t.MailboxSubmission, error)
|
||||
TenantUserProfileGet(t.TenantID, t.Uid) (*t.UserProfile, error)
|
||||
TenantUserProfileUpdate(t.TenantID, t.Uid, t.UserProfilePatch) (*t.UserProfile, error)
|
||||
TenantUserPrivacyGet(t.TenantID, t.Uid) (*t.UserPrivacySettings, error)
|
||||
TenantUserPrivacyUpdate(t.TenantID, t.Uid, t.UserPrivacyPatch) (*t.UserPrivacySettings, error)
|
||||
TenantAppReleaseLatest(t.TenantID, string, string) (*t.AppRelease, error)
|
||||
TenantDevicePushTokenUpsert(t.TenantID, *t.DevicePushToken) (*t.DevicePushToken, error)
|
||||
|
||||
TenantFavoriteCreate(t.TenantID, *t.FavoriteItem) (*t.FavoriteItem, error)
|
||||
TenantFavoriteList(t.TenantID, t.Uid, string, string, string, int) ([]t.FavoriteItem, string, error)
|
||||
@@ -330,6 +363,8 @@ type TenantBusinessAdapter interface {
|
||||
TenantIndependentPasswordGet(t.TenantID, t.Uid, string) (*t.IndependentPassword, error)
|
||||
TenantIndependentPasswordUpsert(t.TenantID, *t.IndependentPassword) error
|
||||
TenantIndependentPasswordUpdateAttempts(t.TenantID, t.Uid, string, int, *time.Time, time.Time) error
|
||||
TenantYouthModeGet(t.TenantID, t.Uid) (*t.YouthModeSettings, error)
|
||||
TenantYouthModeSet(t.TenantID, t.Uid, bool, int, time.Time) (*t.YouthModeSettings, error)
|
||||
|
||||
TenantSystemNotificationCreate(t.TenantID, *t.SystemNotification) error
|
||||
TenantSystemNotificationList(t.TenantID, t.Uid, string, int) ([]t.SystemNotification, string, error)
|
||||
|
||||
@@ -794,6 +794,24 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if err = createIMCallTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMWorkbenchTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMOrganizationTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMMeetingTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMReportTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMMailboxTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMUserProductTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deletion log
|
||||
if _, err = tx.Exec(
|
||||
@@ -950,6 +968,299 @@ func createIMCallTables(tx *sql.Tx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func createIMWorkbenchTables(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_workbench_apps(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
route VARCHAR(128) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
unread_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_workbench_apps_code(code),
|
||||
INDEX idx_im_workbench_apps_enabled_order(enabled, sort_order)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Exec(
|
||||
`INSERT INTO im_workbench_apps(code,name,route,sort_order,unread_key,enabled,permission)
|
||||
VALUES
|
||||
('organization','组织架构','/organization',10,'organization',1,'enabled'),
|
||||
('meetings','会议通知','/meetings',20,'meetings',1,'enabled'),
|
||||
('daily_reports','日报','/reports/daily',30,'daily_reports',1,'enabled'),
|
||||
('monthly_reports','月报','/reports/monthly',40,'monthly_reports',1,'enabled'),
|
||||
('executive_mailbox','总经理信箱','/executive-mailbox',50,'executive_mailbox',1,'enabled')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name=VALUES(name),
|
||||
route=VALUES(route),
|
||||
sort_order=VALUES(sort_order),
|
||||
unread_key=VALUES(unread_key),
|
||||
updated_at=CURRENT_TIMESTAMP(3)`)
|
||||
return err
|
||||
}
|
||||
|
||||
func createIMOrganizationTables(tx *sql.Tx) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE im_org_departments(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_org_departments_department(department_id),
|
||||
INDEX idx_im_org_departments_parent(parent_id, state, sort_order, id),
|
||||
INDEX idx_im_org_departments_name(name)
|
||||
)`,
|
||||
`CREATE TABLE im_org_members(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
display_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
position VARCHAR(128) NOT NULL DEFAULT '',
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
mobile VARCHAR(32) NOT NULL DEFAULT '',
|
||||
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
contactable TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_org_members_department_user(department_id, user_id),
|
||||
INDEX idx_im_org_members_department(department_id, employment_state, sort_order, id),
|
||||
INDEX idx_im_org_members_user(user_id),
|
||||
INDEX idx_im_org_members_display_name(display_name),
|
||||
CONSTRAINT fk_im_org_members_department FOREIGN KEY(department_id) REFERENCES im_org_departments(department_id) ON DELETE CASCADE
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createIMMeetingTables(tx *sql.Tx) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE im_meetings(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
organizer_id BIGINT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
start_at DATETIME(3) NOT NULL,
|
||||
end_at DATETIME(3) NOT NULL,
|
||||
location VARCHAR(256) NOT NULL DEFAULT '',
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
|
||||
reminder_minutes JSON,
|
||||
idempotency_key VARCHAR(128),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
canceled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_meetings_meeting(meeting_id),
|
||||
UNIQUE INDEX uk_im_meetings_idempotency(organizer_id, idempotency_key),
|
||||
INDEX idx_im_meetings_organizer(organizer_id, start_at),
|
||||
INDEX idx_im_meetings_state_time(state, start_at)
|
||||
)`,
|
||||
`CREATE TABLE im_meeting_participants(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
response VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
response_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_meeting_participants_user(meeting_id, user_id),
|
||||
INDEX idx_im_meeting_participants_user_time(user_id, meeting_id),
|
||||
INDEX idx_im_meeting_participants_response(meeting_id, response),
|
||||
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY(meeting_id) REFERENCES im_meetings(meeting_id) ON DELETE CASCADE
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createIMReportTables(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_daily_reports(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_date DATE NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_daily_reports_report(report_id),
|
||||
UNIQUE INDEX uk_im_daily_reports_author_date(author_id, report_date),
|
||||
INDEX idx_im_daily_reports_author_id(author_id, id DESC),
|
||||
INDEX idx_im_daily_reports_date_status(report_date, status, id DESC)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Exec(
|
||||
`CREATE TABLE im_monthly_reports(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_month CHAR(7) NOT NULL,
|
||||
work_summary TEXT NOT NULL,
|
||||
next_plan TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_report(report_id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_author_month(author_id, report_month),
|
||||
INDEX idx_im_monthly_reports_author_id(author_id, id DESC),
|
||||
INDEX idx_im_monthly_reports_month_status(report_month, status, id DESC)
|
||||
)`)
|
||||
return err
|
||||
}
|
||||
|
||||
func createIMMailboxTables(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_mailbox_config(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
allow_anonymous TINYINT NOT NULL DEFAULT 1,
|
||||
required_fields JSON,
|
||||
max_length INT NOT NULL DEFAULT 5000,
|
||||
attachment_policy JSON,
|
||||
notice_title VARCHAR(128) NOT NULL DEFAULT '',
|
||||
notice_content TEXT NOT NULL,
|
||||
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`INSERT INTO im_mailbox_config(id,enabled,allow_anonymous,required_fields,max_length,attachment_policy,notice_title,notice_content)
|
||||
VALUES(1,1,1,'["subject","content"]',5000,'{"max_files":5,"purpose":"executive_mailbox"}','总经理信箱','请如实填写诉求,匿名提交将按企业政策保护展示身份。')
|
||||
ON DUPLICATE KEY UPDATE updated_at=updated_at`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Exec(
|
||||
`CREATE TABLE im_mailbox_submissions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
sender_id BIGINT NOT NULL,
|
||||
subject VARCHAR(200) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
anonymous TINYINT NOT NULL DEFAULT 0,
|
||||
attachment_file_ids JSON,
|
||||
notice_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
submitted_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_submission(submission_id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_idempotency(sender_id, idempotency_key),
|
||||
INDEX idx_im_mailbox_submissions_sender(sender_id, id DESC),
|
||||
INDEX idx_im_mailbox_submissions_status(status, id DESC)
|
||||
)`)
|
||||
return err
|
||||
}
|
||||
|
||||
func createIMUserProductTables(tx *sql.Tx) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE im_user_profiles(
|
||||
user_id BIGINT NOT NULL,
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
birth_date DATE,
|
||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(user_id)
|
||||
)`,
|
||||
`CREATE TABLE im_user_privacy_settings(
|
||||
user_id BIGINT NOT NULL,
|
||||
add_me_methods JSON,
|
||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(user_id)
|
||||
)`,
|
||||
`CREATE TABLE im_app_releases(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
platform VARCHAR(32) NOT NULL,
|
||||
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||
latest_version VARCHAR(64) NOT NULL,
|
||||
min_supported_version VARCHAR(64) NOT NULL,
|
||||
update_type VARCHAR(16) NOT NULL DEFAULT 'none',
|
||||
release_notes TEXT NOT NULL,
|
||||
store_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
package_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
checksum VARCHAR(128) NOT NULL DEFAULT '',
|
||||
remind_after_seconds INT NOT NULL DEFAULT 0,
|
||||
published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_app_releases_platform_channel(platform, channel)
|
||||
)`,
|
||||
`CREATE TABLE im_device_push_tokens(
|
||||
device_id VARCHAR(128) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
provider VARCHAR(32) NOT NULL,
|
||||
token VARCHAR(512) NOT NULL,
|
||||
app_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(device_id),
|
||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token),
|
||||
INDEX idx_im_device_push_tokens_user(user_id, updated_at)
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureTenantRoutingColumns(a *adapter) error {
|
||||
statements := []string{
|
||||
"ALTER TABLE im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc",
|
||||
@@ -1117,6 +1428,10 @@ func createIMSecurityTables(tx *sql.Tx) error {
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 0,
|
||||
policy_version INT NOT NULL DEFAULT 1,
|
||||
restrictions JSON,
|
||||
enabled_at DATETIME(3),
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1583
File diff suppressed because it is too large
Load Diff
+975
-3
File diff suppressed because it is too large
Load Diff
@@ -788,13 +788,37 @@ func main() {
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/security/realname", serveIMRealName)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/security/realname/submissions", serveIMRealNameSubmissions)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/bootstrap", serveIMBootstrap)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/workbench", serveIMWorkbench)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/organization/departments", serveIMOrganizationDepartments)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/organization/departments/", serveIMOrganizationDepartmentResource)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/organization/search", serveIMOrganizationSearch)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/meetings", serveIMMeetings)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/meetings/", serveIMMeetingResource)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily/statistics", serveIMDailyReportStatistics)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily/reminders", serveIMDailyReportReminders)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily", serveIMDailyReports)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/reports/daily/", serveIMDailyReportResource)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/reports/monthly/statistics", serveIMMonthlyReportStatistics)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/reports/monthly", serveIMMonthlyReports)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/reports/monthly/", serveIMMonthlyReportResource)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/executive-mailbox/config", serveIMMailboxConfig)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/executive-mailbox/submissions", serveIMMailboxSubmissions)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/app/releases/latest", serveIMAppReleaseLatest)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/me", serveIMMe)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/me/profile", serveIMMeProfile)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/me/privacy", serveIMMePrivacy)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/settings/youth-mode/enable", serveIMYouthModeEnable)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/settings/youth-mode/disable", serveIMYouthModeDisable)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/settings/youth-mode", serveIMYouthMode)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/settings/independent-password", serveIMIndependentPassword)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/settings/independent-password/verify", serveIMIndependentPasswordVerify)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/devices/", serveIMDevicePushToken)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/search/conversations", serveIMConversationSearch)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications", serveIMSystemNotifications)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications/", serveIMSystemNotificationRead)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/favorites", serveIMFavorites)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/favorites/", serveIMFavoriteResource)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/calls", serveIMCalls)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/conversations/", serveIMConversationContext)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/groups", serveIMGroups)
|
||||
|
||||
@@ -1018,6 +1018,7 @@ type CallsPersistenceInterface interface {
|
||||
Create(tenantID types.TenantID, record *types.CallRecord) error
|
||||
Accept(tenantID types.TenantID, callID string, seq int, acceptedAt time.Time) error
|
||||
Finish(tenantID types.TenantID, callID string, seq int, status string, durationMS int64, finishedAt time.Time) error
|
||||
List(tenantID types.TenantID, uid types.Uid, topic, cursor string, limit int) ([]types.CallRecord, string, error)
|
||||
}
|
||||
|
||||
type callsMapper struct{}
|
||||
@@ -1064,6 +1065,521 @@ func (callsMapper) Finish(tenantID types.TenantID, callID string, seq int, statu
|
||||
return tenantAdp.TenantCallFinish(tenantID, callID, seq, status, durationMS, finishedAt)
|
||||
}
|
||||
|
||||
func (callsMapper) List(tenantID types.TenantID, uid types.Uid, topic, cursor string, limit int) ([]types.CallRecord, string, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantCallList(tenantID, uid, strings.TrimSpace(topic), strings.TrimSpace(cursor), limit)
|
||||
}
|
||||
|
||||
type WorkbenchPersistenceInterface interface {
|
||||
Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error)
|
||||
}
|
||||
|
||||
type workbenchMapper struct{}
|
||||
|
||||
var Workbench WorkbenchPersistenceInterface
|
||||
|
||||
func (workbenchMapper) Get(tenantID types.TenantID, uid types.Uid) ([]types.WorkbenchModule, int, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, 0, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return tenantAdp.TenantWorkbenchModules(tenantID, uid)
|
||||
}
|
||||
|
||||
type OrganizationPersistenceInterface interface {
|
||||
ListDepartments(tenantID types.TenantID, uid types.Uid, parentID, cursor string, limit int) ([]types.OrganizationDepartment, string, error)
|
||||
ListMembers(tenantID types.TenantID, uid types.Uid, departmentID, cursor string, limit int) ([]types.OrganizationMember, string, error)
|
||||
Search(tenantID types.TenantID, uid types.Uid, queryText, resultTypes, cursor string, limit int) ([]types.OrganizationSearchItem, string, error)
|
||||
}
|
||||
|
||||
type organizationMapper struct{}
|
||||
|
||||
var Organization OrganizationPersistenceInterface
|
||||
|
||||
func (organizationMapper) ListDepartments(tenantID types.TenantID, uid types.Uid, parentID, cursor string, limit int) ([]types.OrganizationDepartment, string, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantOrganizationDepartments(tenantID, uid, strings.TrimSpace(parentID), strings.TrimSpace(cursor), limit)
|
||||
}
|
||||
|
||||
func (organizationMapper) ListMembers(tenantID types.TenantID, uid types.Uid, departmentID, cursor string, limit int) ([]types.OrganizationMember, string, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(departmentID) == "" {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantOrganizationMembers(tenantID, uid, strings.TrimSpace(departmentID), strings.TrimSpace(cursor), limit)
|
||||
}
|
||||
|
||||
func (organizationMapper) Search(tenantID types.TenantID, uid types.Uid, queryText, resultTypes, cursor string, limit int) ([]types.OrganizationSearchItem, string, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(queryText) == "" {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantOrganizationSearch(tenantID, uid, strings.TrimSpace(queryText), strings.TrimSpace(resultTypes), strings.TrimSpace(cursor), limit)
|
||||
}
|
||||
|
||||
type MeetingsPersistenceInterface interface {
|
||||
Create(tenantID types.TenantID, uid types.Uid, meeting *types.Meeting, attendeeIDs []types.Uid) (*types.Meeting, error)
|
||||
Get(tenantID types.TenantID, uid types.Uid, meetingID string) (*types.Meeting, error)
|
||||
Update(tenantID types.TenantID, uid types.Uid, meetingID string, patch types.MeetingPatch, idempotencyKey string) (*types.Meeting, error)
|
||||
Cancel(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) (*types.Meeting, error)
|
||||
Remind(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) ([]types.MeetingParticipant, error)
|
||||
Respond(tenantID types.TenantID, uid types.Uid, meetingID, response string, version int) (*types.Meeting, error)
|
||||
}
|
||||
|
||||
type meetingsMapper struct{}
|
||||
|
||||
var Meetings MeetingsPersistenceInterface
|
||||
|
||||
func (meetingsMapper) Create(tenantID types.TenantID, uid types.Uid, meeting *types.Meeting, attendeeIDs []types.Uid) (*types.Meeting, error) {
|
||||
if uid.IsZero() || meeting == nil || !validMeetingForCreate(meeting) {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMeetingCreate(tenantID, uid, meeting, attendeeIDs)
|
||||
}
|
||||
|
||||
func (meetingsMapper) Get(tenantID types.TenantID, uid types.Uid, meetingID string) (*types.Meeting, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMeetingGet(tenantID, uid, strings.TrimSpace(meetingID))
|
||||
}
|
||||
|
||||
func (meetingsMapper) Update(tenantID types.TenantID, uid types.Uid, meetingID string, patch types.MeetingPatch, idempotencyKey string) (*types.Meeting, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || patch.Version <= 0 || !validMeetingPatch(patch) {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMeetingUpdate(tenantID, uid, strings.TrimSpace(meetingID), patch, strings.TrimSpace(idempotencyKey))
|
||||
}
|
||||
|
||||
func (meetingsMapper) Cancel(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) (*types.Meeting, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMeetingCancel(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(idempotencyKey))
|
||||
}
|
||||
|
||||
func (meetingsMapper) Remind(tenantID types.TenantID, uid types.Uid, meetingID, idempotencyKey string) ([]types.MeetingParticipant, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(meetingID) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMeetingRemind(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(idempotencyKey))
|
||||
}
|
||||
|
||||
func (meetingsMapper) Respond(tenantID types.TenantID, uid types.Uid, meetingID, response string, version int) (*types.Meeting, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(meetingID) == "" || !validMeetingResponse(response) || version <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMeetingRespond(tenantID, uid, strings.TrimSpace(meetingID), strings.TrimSpace(response), version)
|
||||
}
|
||||
|
||||
func validMeetingForCreate(meeting *types.Meeting) bool {
|
||||
return strings.TrimSpace(meeting.MeetingID) != "" &&
|
||||
strings.TrimSpace(meeting.Title) != "" &&
|
||||
!meeting.StartAt.IsZero() &&
|
||||
!meeting.EndAt.IsZero() &&
|
||||
meeting.EndAt.After(meeting.StartAt)
|
||||
}
|
||||
|
||||
func validMeetingPatch(patch types.MeetingPatch) bool {
|
||||
if patch.Title == nil && patch.StartAt == nil && patch.EndAt == nil && patch.Location == nil && patch.ReminderMinutes == nil {
|
||||
return false
|
||||
}
|
||||
if patch.Title != nil && strings.TrimSpace(*patch.Title) == "" {
|
||||
return false
|
||||
}
|
||||
if patch.StartAt != nil && patch.EndAt != nil && !patch.EndAt.After(*patch.StartAt) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validMeetingResponse(response string) bool {
|
||||
switch strings.TrimSpace(response) {
|
||||
case types.MeetingResponseAccepted, types.MeetingResponseDeclined, types.MeetingResponseTentative:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type DailyReportsPersistenceInterface interface {
|
||||
List(tenantID types.TenantID, uid types.Uid, scope, from, to, status, cursor string, limit int) ([]types.DailyReport, string, error)
|
||||
CreateDraft(tenantID types.TenantID, uid types.Uid, reportID string, reportDate time.Time) (*types.DailyReport, error)
|
||||
Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.DailyReport, error)
|
||||
SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, content string, attachments []string, version int) (*types.DailyReport, error)
|
||||
Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.DailyReport, error)
|
||||
Stats(tenantID types.TenantID, uid types.Uid, date time.Time, departmentID string) (*types.DailyReportStats, error)
|
||||
Remind(tenantID types.TenantID, uid types.Uid, date time.Time, userIDs []types.Uid, idempotencyKey string) ([]types.Uid, error)
|
||||
}
|
||||
|
||||
type dailyReportsMapper struct{}
|
||||
|
||||
var DailyReports DailyReportsPersistenceInterface
|
||||
|
||||
func (dailyReportsMapper) List(tenantID types.TenantID, uid types.Uid, scope, from, to, status, cursor string, limit int) ([]types.DailyReport, string, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantDailyReportList(tenantID, uid, strings.TrimSpace(scope), strings.TrimSpace(from), strings.TrimSpace(to), strings.TrimSpace(status), strings.TrimSpace(cursor), limit)
|
||||
}
|
||||
|
||||
func (dailyReportsMapper) CreateDraft(tenantID types.TenantID, uid types.Uid, reportID string, reportDate time.Time) (*types.DailyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" || reportDate.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantDailyReportCreateDraft(tenantID, uid, strings.TrimSpace(reportID), reportDate)
|
||||
}
|
||||
|
||||
func (dailyReportsMapper) Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.DailyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantDailyReportGet(tenantID, uid, strings.TrimSpace(reportID))
|
||||
}
|
||||
|
||||
func (dailyReportsMapper) SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, content string, attachments []string, version int) (*types.DailyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantDailyReportSaveDraft(tenantID, uid, strings.TrimSpace(reportID), content, attachments, version)
|
||||
}
|
||||
|
||||
func (dailyReportsMapper) Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.DailyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantDailyReportSubmit(tenantID, uid, strings.TrimSpace(reportID), version)
|
||||
}
|
||||
|
||||
func (dailyReportsMapper) Stats(tenantID types.TenantID, uid types.Uid, date time.Time, departmentID string) (*types.DailyReportStats, error) {
|
||||
if uid.IsZero() || date.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantDailyReportStats(tenantID, uid, date, strings.TrimSpace(departmentID))
|
||||
}
|
||||
|
||||
func (dailyReportsMapper) Remind(tenantID types.TenantID, uid types.Uid, date time.Time, userIDs []types.Uid, idempotencyKey string) ([]types.Uid, error) {
|
||||
if uid.IsZero() || date.IsZero() || strings.TrimSpace(idempotencyKey) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantDailyReportRemind(tenantID, uid, date, userIDs, strings.TrimSpace(idempotencyKey))
|
||||
}
|
||||
|
||||
type MonthlyReportsPersistenceInterface interface {
|
||||
List(tenantID types.TenantID, uid types.Uid, scope, fromMonth, toMonth, status, cursor string, limit int) ([]types.MonthlyReport, string, error)
|
||||
CreateDraft(tenantID types.TenantID, uid types.Uid, reportID, month string) (*types.MonthlyReport, error)
|
||||
Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.MonthlyReport, error)
|
||||
SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, workSummary, nextPlan string, attachments []string, version int) (*types.MonthlyReport, error)
|
||||
Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error)
|
||||
Stats(tenantID types.TenantID, uid types.Uid, month, departmentID string) (*types.MonthlyReportStats, error)
|
||||
}
|
||||
|
||||
type monthlyReportsMapper struct{}
|
||||
|
||||
var MonthlyReports MonthlyReportsPersistenceInterface
|
||||
|
||||
func (monthlyReportsMapper) List(tenantID types.TenantID, uid types.Uid, scope, fromMonth, toMonth, status, cursor string, limit int) ([]types.MonthlyReport, string, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, "", types.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return tenantAdp.TenantMonthlyReportList(tenantID, uid, strings.TrimSpace(scope), strings.TrimSpace(fromMonth), strings.TrimSpace(toMonth), strings.TrimSpace(status), strings.TrimSpace(cursor), limit)
|
||||
}
|
||||
|
||||
func (monthlyReportsMapper) CreateDraft(tenantID types.TenantID, uid types.Uid, reportID, month string) (*types.MonthlyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" || strings.TrimSpace(month) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMonthlyReportCreateDraft(tenantID, uid, strings.TrimSpace(reportID), strings.TrimSpace(month))
|
||||
}
|
||||
|
||||
func (monthlyReportsMapper) Get(tenantID types.TenantID, uid types.Uid, reportID string) (*types.MonthlyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMonthlyReportGet(tenantID, uid, strings.TrimSpace(reportID))
|
||||
}
|
||||
|
||||
func (monthlyReportsMapper) SaveDraft(tenantID types.TenantID, uid types.Uid, reportID, workSummary, nextPlan string, attachments []string, version int) (*types.MonthlyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMonthlyReportSaveDraft(tenantID, uid, strings.TrimSpace(reportID), workSummary, nextPlan, attachments, version)
|
||||
}
|
||||
|
||||
func (monthlyReportsMapper) Submit(tenantID types.TenantID, uid types.Uid, reportID string, version int) (*types.MonthlyReport, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(reportID) == "" || version <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMonthlyReportSubmit(tenantID, uid, strings.TrimSpace(reportID), version)
|
||||
}
|
||||
|
||||
func (monthlyReportsMapper) Stats(tenantID types.TenantID, uid types.Uid, month, departmentID string) (*types.MonthlyReportStats, error) {
|
||||
if uid.IsZero() || strings.TrimSpace(month) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMonthlyReportStats(tenantID, uid, strings.TrimSpace(month), strings.TrimSpace(departmentID))
|
||||
}
|
||||
|
||||
type MailboxPersistenceInterface interface {
|
||||
Config(tenantID types.TenantID, uid types.Uid) (*types.MailboxConfig, error)
|
||||
Submit(tenantID types.TenantID, uid types.Uid, submission *types.MailboxSubmission, idempotencyKey string) (*types.MailboxSubmission, error)
|
||||
}
|
||||
|
||||
type mailboxMapper struct{}
|
||||
|
||||
var Mailbox MailboxPersistenceInterface
|
||||
|
||||
func (mailboxMapper) Config(tenantID types.TenantID, uid types.Uid) (*types.MailboxConfig, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMailboxConfigGet(tenantID, uid)
|
||||
}
|
||||
|
||||
func (mailboxMapper) Submit(tenantID types.TenantID, uid types.Uid, submission *types.MailboxSubmission, idempotencyKey string) (*types.MailboxSubmission, error) {
|
||||
if uid.IsZero() || submission == nil || strings.TrimSpace(submission.SubmissionID) == "" ||
|
||||
strings.TrimSpace(submission.Subject) == "" || strings.TrimSpace(submission.Content) == "" ||
|
||||
strings.TrimSpace(idempotencyKey) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantMailboxSubmissionCreate(tenantID, uid, submission, strings.TrimSpace(idempotencyKey))
|
||||
}
|
||||
|
||||
type ProfilePersistenceInterface interface {
|
||||
Get(tenantID types.TenantID, uid types.Uid) (*types.UserProfile, error)
|
||||
Update(tenantID types.TenantID, uid types.Uid, patch types.UserProfilePatch) (*types.UserProfile, error)
|
||||
GetPrivacy(tenantID types.TenantID, uid types.Uid) (*types.UserPrivacySettings, error)
|
||||
UpdatePrivacy(tenantID types.TenantID, uid types.Uid, patch types.UserPrivacyPatch) (*types.UserPrivacySettings, error)
|
||||
}
|
||||
|
||||
type profileMapper struct{}
|
||||
|
||||
var Profiles ProfilePersistenceInterface
|
||||
|
||||
type AppReleasePersistenceInterface interface {
|
||||
Latest(tenantID types.TenantID, platform, channel string) (*types.AppRelease, error)
|
||||
}
|
||||
|
||||
type appReleaseMapper struct{}
|
||||
|
||||
var AppReleases AppReleasePersistenceInterface
|
||||
|
||||
func (appReleaseMapper) Latest(tenantID types.TenantID, platform, channel string) (*types.AppRelease, error) {
|
||||
platform = strings.ToLower(strings.TrimSpace(platform))
|
||||
channel = strings.ToLower(strings.TrimSpace(channel))
|
||||
if platform == "" || channel == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantAppReleaseLatest(tenantID, platform, channel)
|
||||
}
|
||||
|
||||
type DevicePushTokenPersistenceInterface interface {
|
||||
Upsert(tenantID types.TenantID, token *types.DevicePushToken) (*types.DevicePushToken, error)
|
||||
}
|
||||
|
||||
type devicePushTokenMapper struct{}
|
||||
|
||||
var DevicePushTokens DevicePushTokenPersistenceInterface
|
||||
|
||||
func (devicePushTokenMapper) Upsert(tenantID types.TenantID, token *types.DevicePushToken) (*types.DevicePushToken, error) {
|
||||
if token == nil || token.UserID.IsZero() || strings.TrimSpace(token.DeviceID) == "" ||
|
||||
strings.TrimSpace(token.Provider) == "" || strings.TrimSpace(token.Token) == "" {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantDevicePushTokenUpsert(tenantID, token)
|
||||
}
|
||||
|
||||
func (profileMapper) Get(tenantID types.TenantID, uid types.Uid) (*types.UserProfile, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantUserProfileGet(tenantID, uid)
|
||||
}
|
||||
|
||||
func (profileMapper) Update(tenantID types.TenantID, uid types.Uid, patch types.UserProfilePatch) (*types.UserProfile, error) {
|
||||
if uid.IsZero() || patch.Version <= 0 || !validUserProfilePatch(patch) {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantUserProfileUpdate(tenantID, uid, patch)
|
||||
}
|
||||
|
||||
func (profileMapper) GetPrivacy(tenantID types.TenantID, uid types.Uid) (*types.UserPrivacySettings, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantUserPrivacyGet(tenantID, uid)
|
||||
}
|
||||
|
||||
func (profileMapper) UpdatePrivacy(tenantID types.TenantID, uid types.Uid, patch types.UserPrivacyPatch) (*types.UserPrivacySettings, error) {
|
||||
if uid.IsZero() || patch.Version <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantUserPrivacyUpdate(tenantID, uid, patch)
|
||||
}
|
||||
|
||||
func validUserProfilePatch(patch types.UserProfilePatch) bool {
|
||||
if patch.AvatarFileID == nil && patch.Nickname == nil && patch.BirthDate == nil && patch.Signature == nil && patch.Gender == nil && patch.Region == nil {
|
||||
return false
|
||||
}
|
||||
if patch.Nickname != nil {
|
||||
name := strings.TrimSpace(*patch.Nickname)
|
||||
if len([]rune(name)) == 0 || len([]rune(name)) > 32 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if patch.Signature != nil && len([]rune(*patch.Signature)) > 120 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type RelationsPersistenceInterface interface {
|
||||
CreateFriendRequest(tenantID types.TenantID, req *types.FriendRequest) (*types.FriendRequest, error)
|
||||
ListFriendRequests(tenantID types.TenantID, uid types.Uid, direction, status, cursor string, limit int) ([]types.FriendRequestListItem, string, error)
|
||||
@@ -2015,6 +2531,8 @@ type UserSecurityPersistenceInterface interface {
|
||||
GetIndependentPassword(tenantID types.TenantID, uid types.Uid, purpose string) (*types.IndependentPassword, error)
|
||||
UpsertIndependentPassword(tenantID types.TenantID, pass *types.IndependentPassword) error
|
||||
UpdateIndependentPasswordAttempts(tenantID types.TenantID, uid types.Uid, purpose string, attempts int, lockedUntil *time.Time, updatedAt time.Time) error
|
||||
GetYouthMode(tenantID types.TenantID, uid types.Uid) (*types.YouthModeSettings, error)
|
||||
SetYouthMode(tenantID types.TenantID, uid types.Uid, enabled bool, policyVersion int, now time.Time) (*types.YouthModeSettings, error)
|
||||
CreateSystemNotification(tenantID types.TenantID, notification *types.SystemNotification) error
|
||||
ListSystemNotifications(tenantID types.TenantID, uid types.Uid, cursor string, limit int) ([]types.SystemNotification, string, error)
|
||||
MarkSystemNotificationRead(tenantID types.TenantID, uid types.Uid, eventID string, readAt time.Time) error
|
||||
@@ -2106,6 +2624,28 @@ func (userSecurityMapper) UpdateIndependentPasswordAttempts(tenantID types.Tenan
|
||||
return tenantAdp.TenantIndependentPasswordUpdateAttempts(tenantID, uid, purpose, attempts, lockedUntil, updatedAt)
|
||||
}
|
||||
|
||||
func (userSecurityMapper) GetYouthMode(tenantID types.TenantID, uid types.Uid) (*types.YouthModeSettings, error) {
|
||||
if uid.IsZero() {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantYouthModeGet(tenantID, uid)
|
||||
}
|
||||
|
||||
func (userSecurityMapper) SetYouthMode(tenantID types.TenantID, uid types.Uid, enabled bool, policyVersion int, now time.Time) (*types.YouthModeSettings, error) {
|
||||
if uid.IsZero() || policyVersion <= 0 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tenantAdp.TenantYouthModeSet(tenantID, uid, enabled, policyVersion, now)
|
||||
}
|
||||
|
||||
func (userSecurityMapper) CreateSystemNotification(tenantID types.TenantID, notification *types.SystemNotification) error {
|
||||
if notification == nil || notification.TenantID != tenantID {
|
||||
return types.ErrMalformed
|
||||
@@ -2232,6 +2772,15 @@ func init() {
|
||||
Subs = subsMapper{}
|
||||
Messages = messagesMapper{}
|
||||
Calls = callsMapper{}
|
||||
Workbench = workbenchMapper{}
|
||||
Organization = organizationMapper{}
|
||||
Meetings = meetingsMapper{}
|
||||
DailyReports = dailyReportsMapper{}
|
||||
MonthlyReports = monthlyReportsMapper{}
|
||||
Mailbox = mailboxMapper{}
|
||||
Profiles = profileMapper{}
|
||||
AppReleases = appReleaseMapper{}
|
||||
DevicePushTokens = devicePushTokenMapper{}
|
||||
Devices = deviceMapper{}
|
||||
Files = fileMapper{}
|
||||
PCache = pcacheMapper{}
|
||||
|
||||
@@ -12,6 +12,7 @@ const (
|
||||
)
|
||||
|
||||
type CallRecord struct {
|
||||
ID int64 `db:"id"`
|
||||
TenantID TenantID `db:"tenant_id" json:"-" bson:"tenant_id"`
|
||||
CallID string `db:"call_id"`
|
||||
Topic string
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
const MailboxSubmissionStatusReceived = "received"
|
||||
|
||||
type MailboxNotice struct {
|
||||
Title string
|
||||
Content string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type MailboxConfig struct {
|
||||
Enabled bool
|
||||
AllowAnonymous bool
|
||||
RequiredFields []string
|
||||
MaxLength int
|
||||
AttachmentPolicy map[string]any
|
||||
Notice MailboxNotice
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type MailboxSubmission struct {
|
||||
ID int64
|
||||
SubmissionID string
|
||||
SenderID Uid
|
||||
Subject string
|
||||
Content string
|
||||
Anonymous bool
|
||||
AttachmentFileIDs []string
|
||||
NoticeVersion string
|
||||
Status string
|
||||
SubmittedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
MeetingStateScheduled = "scheduled"
|
||||
MeetingStateCanceled = "canceled"
|
||||
|
||||
MeetingResponsePending = "pending"
|
||||
MeetingResponseAccepted = "accepted"
|
||||
MeetingResponseDeclined = "declined"
|
||||
MeetingResponseTentative = "tentative"
|
||||
)
|
||||
|
||||
type Meeting struct {
|
||||
ID int64
|
||||
MeetingID string
|
||||
OrganizerID Uid
|
||||
Title string
|
||||
StartAt time.Time
|
||||
EndAt time.Time
|
||||
Location string
|
||||
State string
|
||||
ReminderMinutes []int
|
||||
IdempotencyKey string
|
||||
Version int
|
||||
CanceledAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Participants []MeetingParticipant
|
||||
}
|
||||
|
||||
type MeetingParticipant struct {
|
||||
ID int64
|
||||
MeetingID string
|
||||
UserID Uid
|
||||
Response string
|
||||
ResponseAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type MeetingPatch struct {
|
||||
Title *string
|
||||
StartAt *time.Time
|
||||
EndAt *time.Time
|
||||
Location *string
|
||||
ReminderMinutes []int
|
||||
Version int
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type OrganizationDepartment struct {
|
||||
ID int64 `db:"id"`
|
||||
DepartmentID string `db:"department_id"`
|
||||
Name string `db:"name"`
|
||||
ParentID string `db:"parent_id"`
|
||||
SortOrder int `db:"sort_order"`
|
||||
ChildCount int `db:"child_count"`
|
||||
MemberCount int `db:"member_count"`
|
||||
Version int `db:"version"`
|
||||
State string `db:"state"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type OrganizationMember struct {
|
||||
ID int64 `db:"id"`
|
||||
DepartmentID string `db:"department_id"`
|
||||
UserID Uid `db:"-"`
|
||||
DisplayName string `db:"display_name"`
|
||||
Position string `db:"position"`
|
||||
AvatarFileID string `db:"avatar_file_id"`
|
||||
Mobile string `db:"mobile"`
|
||||
Email string `db:"email"`
|
||||
Status string `db:"employment_state"`
|
||||
Contactable bool `db:"contactable"`
|
||||
SortOrder int `db:"sort_order"`
|
||||
Version int `db:"version"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
type OrganizationSearchItem struct {
|
||||
Type string
|
||||
Department *OrganizationDepartment
|
||||
Member *OrganizationMember
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type UserProfile struct {
|
||||
UserID Uid
|
||||
AvatarFileID string
|
||||
Nickname string
|
||||
BirthDate string
|
||||
Signature string
|
||||
Gender string
|
||||
Region string
|
||||
PublicAccount string
|
||||
Version int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type UserProfilePatch struct {
|
||||
AvatarFileID *string
|
||||
Nickname *string
|
||||
BirthDate *string
|
||||
Signature *string
|
||||
Gender *string
|
||||
Region *string
|
||||
Version int
|
||||
}
|
||||
|
||||
type UserPrivacySettings struct {
|
||||
UserID Uid
|
||||
AddMeMethods map[string]bool
|
||||
ProfileVisibility string
|
||||
LastSeenVisibility string
|
||||
ReadReceipts bool
|
||||
Version int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type UserPrivacyPatch struct {
|
||||
AddMeMethods map[string]bool
|
||||
ProfileVisibility *string
|
||||
LastSeenVisibility *string
|
||||
ReadReceipts *bool
|
||||
Version int
|
||||
}
|
||||
|
||||
type AppRelease struct {
|
||||
ID int64
|
||||
Platform string
|
||||
Channel string
|
||||
LatestVersion string
|
||||
MinSupportedVersion string
|
||||
UpdateType string
|
||||
ReleaseNotes string
|
||||
StoreURL string
|
||||
PackageURL string
|
||||
Checksum string
|
||||
RemindAfterSeconds int
|
||||
PublishedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type YouthModeSettings struct {
|
||||
UserID Uid
|
||||
Enabled bool
|
||||
PolicyVersion int
|
||||
Restrictions []string
|
||||
PasswordSet bool
|
||||
CanEnable bool
|
||||
EnabledAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type DevicePushToken struct {
|
||||
UserID Uid
|
||||
DeviceID string
|
||||
Provider string
|
||||
Token string
|
||||
AppVersion string
|
||||
Platform string
|
||||
UpdatedAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
DailyReportStatusDraft = "draft"
|
||||
DailyReportStatusSubmitted = "submitted"
|
||||
)
|
||||
|
||||
type DailyReport struct {
|
||||
ID int64
|
||||
ReportID string
|
||||
AuthorID Uid
|
||||
ReportDate time.Time
|
||||
Content string
|
||||
Attachments []string
|
||||
Status string
|
||||
ReadState string
|
||||
Version int
|
||||
SubmittedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type DailyReportStats struct {
|
||||
ExpectedCount int
|
||||
SubmittedCount int
|
||||
MissingCount int
|
||||
}
|
||||
|
||||
type MonthlyReport struct {
|
||||
ID int64
|
||||
ReportID string
|
||||
AuthorID Uid
|
||||
Month string
|
||||
WorkSummary string
|
||||
NextPlan string
|
||||
Attachments []string
|
||||
Status string
|
||||
ReadState string
|
||||
Version int
|
||||
SubmittedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type MonthlyReportStats struct {
|
||||
ExpectedCount int
|
||||
SubmittedCount int
|
||||
MissingCount int
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type WorkbenchModule struct {
|
||||
ID int64 `db:"id"`
|
||||
Code string `db:"code"`
|
||||
Name string `db:"name"`
|
||||
IconFileID string `db:"icon_file_id"`
|
||||
Route string `db:"route"`
|
||||
SortOrder int `db:"sort_order"`
|
||||
UnreadKey string `db:"unread_key"`
|
||||
Enabled bool `db:"enabled"`
|
||||
Permission string `db:"permission"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
@@ -214,6 +214,7 @@ type testSecurityStore struct {
|
||||
realName *types.RealNameVerification
|
||||
actionToken *types.ActionToken
|
||||
independentPassword *types.IndependentPassword
|
||||
youthMode *types.YouthModeSettings
|
||||
attempts int
|
||||
lockedUntil *time.Time
|
||||
notifications []types.SystemNotification
|
||||
@@ -292,6 +293,46 @@ func (s *testSecurityStore) UpdateIndependentPasswordAttempts(_ types.TenantID,
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *testSecurityStore) GetYouthMode(_ types.TenantID, uid types.Uid) (*types.YouthModeSettings, error) {
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
if s.youthMode == nil {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
s.youthMode = &types.YouthModeSettings{
|
||||
UserID: uid,
|
||||
Enabled: false,
|
||||
PolicyVersion: 1,
|
||||
Restrictions: []string{"night_quiet", "content_filter", "payment_guard"},
|
||||
PasswordSet: s.independentPassword != nil,
|
||||
CanEnable: s.independentPassword != nil,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
return s.youthMode, nil
|
||||
}
|
||||
|
||||
func (s *testSecurityStore) SetYouthMode(_ types.TenantID, uid types.Uid, enabled bool, policyVersion int, now time.Time) (*types.YouthModeSettings, error) {
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
if s.youthMode == nil {
|
||||
s.youthMode = &types.YouthModeSettings{UserID: uid, Restrictions: []string{"night_quiet", "content_filter", "payment_guard"}}
|
||||
}
|
||||
s.youthMode.Enabled = enabled
|
||||
s.youthMode.PolicyVersion = policyVersion
|
||||
s.youthMode.PasswordSet = true
|
||||
s.youthMode.CanEnable = !enabled
|
||||
s.youthMode.UpdatedAt = now
|
||||
if enabled {
|
||||
s.youthMode.EnabledAt = &now
|
||||
} else {
|
||||
s.youthMode.EnabledAt = nil
|
||||
}
|
||||
return s.youthMode, nil
|
||||
}
|
||||
|
||||
func (s *testSecurityStore) CreateSystemNotification(_ types.TenantID, notification *types.SystemNotification) error {
|
||||
if notification != nil {
|
||||
s.notifications = append(s.notifications, *notification)
|
||||
|
||||
Reference in New Issue
Block a user