feat(mobile,account-service): 统一发送服务 + 员工目录接口 + 好友申请五态/验证消息(B-52 施工 2/3)

- message_send_service:所有消息走统一门禁(登录/连接/空接收方/好友校验),
  失败打印 SDK 原始 code 并映射中文提示,不再只吞异常
- chat_screen 接入统一发送服务,失败提示真实原因
- account-service 新增 GET /api/directory(登录用户可读启用员工,用于添加同事)
- add_friend_screen 增加可编辑验证消息(视觉规范 4.2)
- new_friends_screen 五态 + 窄屏(<360)按钮组换行不挤压姓名(视觉规范 5.3)

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
编码工程师
2026-08-16 02:16:00 +08:00
co-authored by multica-agent
parent d6ef6eb9a4
commit 12b0a1c943
5 changed files with 232 additions and 41 deletions
+27
View File
@@ -203,6 +203,11 @@ function readBody(req) {
});
}
/** 从已解析的 URL 里取查询参数(原样返回,未解码处理由调用方负责) */
function urlParam(req, key) {
return req.urlObj ? (req.urlObj.searchParams.get(key) || '') : '';
}
function checkAdmin(req, res) {
const t = req.headers['admin-token'] || '';
const a = Buffer.from(String(t));
@@ -221,6 +226,27 @@ function publicView(emp, staffNo) {
const routes = {
'GET /api/health': async (req, res) => ok(res, { status: 'up', employees: Object.keys(employees).length }),
// 员工目录:登录用户可读的启用员工列表(供客户端「添加同事」搜索)。
// 请求头 Authorization: Bearer <OpenIM imToken>(与 /api/rtc_token 一致,用 parse_token 校验身份)。
// 只返回启用员工、排除自己,不返回密码/手机号/管理字段。
'GET /api/directory': async (req, res) => {
const m = /^Bearer\s+(.+)$/.exec(String(req.headers.authorization || ''));
if (!m) return fail(res, '未登录或登录已过期', 401);
const userID = await parseImToken(m[1]);
if (!userID) return fail(res, '登录已过期,请重新登录', 401);
const keyword = (urlParam(req, 'keyword') || '').trim();
const limit = Math.min(Math.max(parseInt(urlParam(req, 'limit') || '20', 10) || 20, 1), 100);
let list = Object.keys(employees)
.filter((k) => employees[k].status === 'active' && k !== userID)
.map((k) => ({ userID: k, nickname: employees[k].name, department: '', title: '', faceURL: '' }));
if (keyword) {
const kw = keyword.toLowerCase();
list = list.filter((u) => u.userID.toLowerCase().includes(kw) || u.nickname.toLowerCase().includes(kw));
}
const items = list.slice(0, limit);
ok(res, { items, nextCursor: null });
},
'POST /api/login': async (req, res, body) => {
const { staffNo, password, platformID } = body;
if (!staffNo || !password) return fail(res, '工号和密码不能为空');
@@ -334,6 +360,7 @@ const routes = {
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, 'http://localhost');
req.urlObj = url;
if (req.method === 'OPTIONS') return send(res, 204, {});
// 管理页(免登录打开,页面内再输入管理口令)
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/admin')) {