Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c73751889 | ||
|
|
15620bb4bf | ||
|
|
dfe14b29e3 | ||
|
|
8b5d531f3a | ||
|
|
78e319c2f9 | ||
|
|
a1c6ac10e5 | ||
|
|
e3b6edeb7b | ||
|
|
4cd95c61ab | ||
|
|
76d461a7ec | ||
|
|
e2835362e9 | ||
|
|
b008a5754a | ||
|
|
f559012a4c | ||
|
|
e064e86def | ||
|
|
6cf1c0f4a0 | ||
|
|
f95de7eceb | ||
|
|
e408768fe5 | ||
|
|
6639b93c45 | ||
|
|
949945a195 | ||
|
|
a0bae500bc | ||
|
|
c596aa93b5 | ||
|
|
795a433f35 | ||
|
|
7b69c8668f | ||
|
|
f2ac17731a | ||
|
|
5daecc8ae8 | ||
|
|
73bdbf9519 | ||
|
|
52bb9faa27 | ||
|
|
d5bb8646e1 | ||
|
|
ff452d89d8 | ||
|
|
812c8e87f4 | ||
|
|
af22fd0e45 | ||
|
|
3d6d70604d | ||
|
|
0d342a6e30 | ||
|
|
1e42323697 | ||
|
|
40e61cd300 | ||
|
|
0692320dea | ||
|
|
7c2b159aba | ||
|
|
aa4cc26416 | ||
|
|
686ce2daf8 | ||
|
|
b8cc1992b7 | ||
|
|
6b2edcf06c | ||
|
|
da60b0ab99 | ||
|
|
0009c47abb | ||
|
|
dc2367e1cf | ||
|
|
58e51e96a9 |
@@ -9,5 +9,6 @@ front/dist/
|
||||
|
||||
# 本地部署凭据与运行态
|
||||
deploy/.env.dev
|
||||
deploy/remote.conf
|
||||
deploy/app.pid
|
||||
logs/
|
||||
|
||||
@@ -57,6 +57,7 @@ xiaoqu-agent-service/
|
||||
- **ThreadLocal 实测结论**:`spring.ai.mcp.server.type=SYNC`(application.yml 已显式设置)时,Spring AI 1.1.8 的工具回调在 servlet 请求线程(`nio-8088-exec-*`)上同步执行,ThreadLocal 的 AuthContext 与 RequestContextHolder 均有效(已用真实 curl 验证:agent.list.query 按人过滤、audit_log 正确落 user_id/token_id/client_ip)。AkAuthFilter 在请求结束 finally 清理。
|
||||
- **tools/list 按作用域过滤**(2026-09-04 起):MCP SDK 的工具清单在启动时静态注册,无法按请求过滤,故 `ToolsListScopeFilter` 在 servlet 层拦截 POST /mcp 的 tools/list 响应并重写 `result.tools`(只保留该 ak 作用域并集内、且启用状态的工具)。`tools/call` 路径不变:越权调用仍返回可读拒绝信息并落 denied 审计。
|
||||
- client_type 识别:`X-Client-Type` 头优先,否则 User-Agent 含 kimi→mcp-kimi / claude→mcp-claude / curl→cli,其余 unknown。
|
||||
- **统一输出规范**(REQ-20260911-0011 起):schema_version 双跑——`tools/call` 默认(v1)返回原有 content text 不变;请求头 `X-Mcp-Schema-Version: 2` 时 `McpToolCallEnvelopeFilter`(@Order(15))在 servlet 层缓冲响应并写入 `result.structuredContent` 信封(`schema_version`/`request_id`/`data|error`),版本值非法直接返回 `SCHEMA_VERSION_UNSUPPORTED`。所有工具异常在 `GuardedToolCallback` 收敛为 8 个统一错误码(`ToolCallException`,message 即 `{"error":{code,message,request_id}}` JSON),裸异常细节脱敏后只落服务端日志。`tools/list` 重写时同步注入只读/幂等/非破坏性 annotations(`ToolKindRegistry`,源自 `@XqTool.kind`),v2 请求额外注入信封 outputSchema。审计与日志不记录 ak、手机号脱敏(`MaskingUtil`)。
|
||||
|
||||
## 行级数据权限引擎(permission/PermissionService)
|
||||
|
||||
@@ -80,6 +81,10 @@ xiaoqu-agent-service/
|
||||
| `agent.count` | system | read | 查 m0 库 `agent` 表 `soft_delete=0` 总数 |
|
||||
| `agent.list.query` | customer | read | 代理商分页查询。入参 id/name/level/channelNo/province/city/serialnumber/page/rows;出参 id/name/level/channelNo/serialnumber/supervisorName/province/city/countyseat/contact/phone/agencyarea/memo/createTime。行权限:isagent=1 只查直属下级,id 精确查可看自身。supervisorName 批量 IN 富化 |
|
||||
| `property.list.query` | customer | read | 物业(客户)分页查询(m0 properyt 表)。入参 name/agentId/serialnumber/province/city/contact/phone/page/rows;出参 id/name/serialnumber/agentName/createName/province/city/countyseat/address/contact/phone/createTime。行权限走 PermissionService 完整版 getByIsAgent 语义(不含 enterprise_type=2 限制,与老 /property/listByPage 一致)。agentName/createName 批量 IN 富化 |
|
||||
| `integration.context.get` | integration | read | 身份与能力自检(REQ-20260911-0012)。无入参;返回 tenant_id(作用域编码映射)/principal_type/subject_id/scopes(可调用工具集合,与强制校验同源实时计算)/data_scope_version(token_scope 绑定最大修改时间 epoch 毫秒)/capability_levels(QUERY/SYNC,按 *.changes.list 数据驱动)/server_time |
|
||||
| `warehouse.in.list.query` | warehouse | read | 入库单分页查询;入库/申请日期独立过滤,主单代理、涉及仓库、流程参与人三类权限取并集 |
|
||||
| `warehouse.in.detail.query` | warehouse | read | 入库单详情;支持调拨单,物料汇总、可选 SN、流程、附件和关联出库索引 |
|
||||
| `warehouse.todo.list.query` | warehouse | read | 当前用户仓库待办统一查询(REQ-20260915-0008),合并老系统出入库/调拨三个待办接口;流程视图 user_id 过滤,不按代理商过滤;调拨派生出入库单带 derivedFrom 标记 |
|
||||
|
||||
分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 `notice` 字段提示。
|
||||
|
||||
@@ -128,6 +133,15 @@ cp deploy/.env.example deploy/.env.dev # 填写 dev 凭据(已 gitignore,
|
||||
- `.env.dev` 只需管理员引导和 Flyway 开关;数据库/Redis 已走配置文件。日志在 `logs/agent-service.log`,pid 在 `deploy/app.pid`。
|
||||
- 注意应用启动较慢(多数据源初始化,约 60-90s),脚本健康检查窗口已放宽到 180s。
|
||||
|
||||
### 生产部署(prd,HTTPS 已于 2026-09-11 上线)
|
||||
|
||||
- 服务器:SSH 别名 `xiaoqu-prd`(123.57.245.202,Rocky 8),部署目录 `/data/xiaoqu-agent-service`(jar、`app.env`、`logs/`、`app.pid`)。连接参数在 `deploy/remote.conf`(已 gitignore)。
|
||||
- 发布:`./deploy/deploy.sh remote-deploy`(本地构建 → 上传 → 远程停旧起新 → 健康检查);只重启用 `remote-restart`。
|
||||
- 对外地址:MCP 端点 `https://agent.ququclean.com/mcp`,管理后台 `https://agent.ququclean.com/`。
|
||||
- TLS 由 Nginx 终结(配置 `/etc/nginx/conf.d/agent.ququclean.com.conf`,转发 `127.0.0.1:8088`):`/mcp` 单独 location,`proxy_buffering off` + 1 小时读写超时(SSE 流式必需);全站传递 `X-Real-IP` / `X-Forwarded-For` / `X-Forwarded-Proto`;80 端口 301 跳 HTTPS;firewalld 已永久放行 http/https。
|
||||
- 证书:阿里云一年期 DV 证书(DigiCert),文件在 `/etc/nginx/ssl/agent.ququclean.com/`(pem 644 / key 600),**2027-03-11 到期,无自动续期**。到期前在阿里云重新申请下载,替换这两个文件后 `nginx -s reload`。
|
||||
- prd 的 `app.env` 相比 dev 必须多出两项:`ADMIN_COOKIE_SECURE=true`(会话 cookie 仅经 HTTPS 下发)、`SERVER_FORWARD_HEADERS_STRATEGY=framework`(认 X-Forwarded 头,审计 `client_ip` 才能取到真实来源 IP,否则全是 127.0.0.1)。
|
||||
|
||||
## 测试 ak(仅 dev)
|
||||
|
||||
仓库不提供固定 ak。每位开发者在本地生成随机 ak,只把 SHA-256 写入 `agent_api_key`,再用 `token_scope` 关联需要的作用域;明文只保存在本机客户端配置(如 `.kimi-code/mcp.json`,已 gitignore)中,不提交版本库。仓库历史中曾出现的 dev ak 必须视为已泄露并在数据库中禁用。
|
||||
@@ -173,7 +187,7 @@ agent.list.query 抽测:channel_no='D' + province='北京市' → 总部 11 /
|
||||
|
||||
## 客户端接入(dev 阶段裸跑 HTTP)
|
||||
|
||||
dev 服务地址示例:`http://192.168.4.236:8088/mcp`(本机内网 IP;本机自测可用 localhost)。生产必须 HTTPS(Nginx 终结 TLS)。
|
||||
dev 服务地址示例:`http://192.168.4.236:8088/mcp`(本机内网 IP;本机自测可用 localhost)。生产地址 `https://agent.ququclean.com/mcp`(Nginx 终结 TLS,部署细节见上文「生产部署」)。
|
||||
|
||||
**Kimi Code** — 项目级 `.kimi-code/mcp.json`(ak 明文直写;该文件已加入 `.gitignore`,不会提交版本库):
|
||||
|
||||
@@ -201,7 +215,7 @@ claude mcp add --transport http xiaoqu http://192.168.4.236:8088/mcp \
|
||||
|
||||
## Open API(服务间接口,/api/open/**)
|
||||
|
||||
供其他系统在服务端到服务端场景下获取用户 Token。鉴权为静态服务间密钥(header `X-Open-Key`,dev 明文配置在 `application-dev.yml` 的 `app.open.api-key`;配置为空 = 接口关闭)。
|
||||
供其他系统在服务端到服务端场景下获取用户或服务 Token。鉴权使用 header `X-Open-Key`:生产优先在管理后台「服务密钥」页面创建并绑定作用域,明文只展示一次、数据库仅存 SHA-256 哈希;原有 `app.open.api-key` / `app.open.clients` 环境配置继续兼容。
|
||||
|
||||
**`POST /api/open/token/ensure`** —— 按 手机号/邮箱 + 作用域编码 确保 Token 存在(get-or-create):
|
||||
|
||||
@@ -217,12 +231,13 @@ curl -X POST http://localhost:8088/api/open/token/ensure \
|
||||
- 已存在但无密文(历史数据)→ 自动轮换:禁用旧把、签发新把,按 `created=true` 返回新明文。
|
||||
- 不存在 → 自动创建永久 Token → `{"created":true,"token":"xqak_...(明文)",...}`。
|
||||
- 调用写 `audit_log`(`open.token.ensure` / client_type=open-api,不记录明文)。
|
||||
- 管理后台支持服务密钥列表、创建、编辑作用域、吊销/启用和轮换;吊销或轮换后旧密钥立即无法通过 `/api/open/**` 鉴权。
|
||||
|
||||
**签发规则(2026-09-04 起)**:一作用域一 Token。后台签发时选 N 个作用域会生成 N 把 token,每把独立绑定单作用域;编辑存量 token 时作用域为单选。
|
||||
|
||||
## codegen(run-once)
|
||||
|
||||
表白名单:m0 `user, agent, authority, properyt, agent_manage_district_auth`;m2 `warehouse_agent_auth`;svc `agent_api_key, agent_scope, tool, scope_tool, token_scope, audit_log, admin_user`(注:物业表真实表名为老系统遗留拼写 `properyt`)。生成物落在 `src/main/java` 对应分包并提交进仓库。
|
||||
表白名单:m0 `user, agent, authority, properyt, agent_manage_district_auth`;m2 `warehouse_agent_auth`;svc `agent_api_key, agent_scope, tool, scope_tool, token_scope, audit_log, admin_user, app_open_client, app_open_client_scope`(注:物业表真实表名为老系统遗留拼写 `properyt`)。生成物落在 `src/main/java` 对应分包并提交进仓库。
|
||||
|
||||
```bash
|
||||
# 生成(依赖已下载后执行;连 dev 库)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# 数据库/Redis 连接信息已明文写在 application-dev.yml,无需在此配置
|
||||
|
||||
# 后台管理员首次引导(仅首次启动且 admin_user 为空时建号,之后可删除)
|
||||
APP_ADMIN_BOOTSTRAP_ACCOUNT=admin
|
||||
APP_ADMIN_BOOTSTRAP_PASSWORD=
|
||||
# APP_ADMIN_BOOTSTRAP_NAME=系统管理员
|
||||
ADMIN_BOOTSTRAP_ACCOUNT=admin
|
||||
ADMIN_BOOTSTRAP_PASSWORD=
|
||||
# ADMIN_BOOTSTRAP_NAME=系统管理员
|
||||
|
||||
# 默认拒绝为非空库自动建基线;仅已有 V1 库首次接入时临时改为 true
|
||||
SVC_FLYWAY_BASELINE_ON_MIGRATE=false
|
||||
|
||||
@@ -90,6 +90,99 @@ do_status() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- 远程部署 ----------
|
||||
REMOTE_CONF="$APP_HOME/deploy/remote.conf"
|
||||
|
||||
load_remote_conf() {
|
||||
if [ ! -f "$REMOTE_CONF" ]; then
|
||||
echo "[ERROR] 缺少 $REMOTE_CONF,请复制 deploy/remote.conf.example 并填写" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a; source "$REMOTE_CONF"; set +a
|
||||
: "${REMOTE_SSH_HOST:?未配置 REMOTE_SSH_HOST}"
|
||||
: "${REMOTE_DIR:?未配置 REMOTE_DIR}"
|
||||
REMOTE_SSH_PORT="${REMOTE_SSH_PORT:-22}"
|
||||
REMOTE_PROFILE="${REMOTE_PROFILE:-prod}"
|
||||
REMOTE_PORT="${REMOTE_PORT:-8088}"
|
||||
REMOTE_JAVA_OPTS="${REMOTE_JAVA_OPTS:-}"
|
||||
REMOTE_SPRING_ARGS="${REMOTE_SPRING_ARGS:-}"
|
||||
REMOTE_JAVA_BIN="${REMOTE_JAVA_BIN:-java}"
|
||||
}
|
||||
|
||||
ssh_remote() { ssh -p "$REMOTE_SSH_PORT" "$REMOTE_SSH_HOST" "$@"; }
|
||||
|
||||
do_push() {
|
||||
[ -f "$JAR" ] || { echo "[ERROR] 未找到 jar,先执行: $0 build" >&2; exit 1; }
|
||||
load_remote_conf
|
||||
ssh_remote "mkdir -p '$REMOTE_DIR/logs'"
|
||||
scp -P "$REMOTE_SSH_PORT" "$JAR" "$REMOTE_SSH_HOST:$REMOTE_DIR/xiaoqu-agent-service.jar.new"
|
||||
echo "[OK] 已上传到 $REMOTE_SSH_HOST:$REMOTE_DIR/xiaoqu-agent-service.jar.new"
|
||||
}
|
||||
|
||||
# 在远程执行 停旧→(可选)换包→启动→健康检查;$1: 1=换上新传的 jar,0=仅重启现有 jar
|
||||
run_remote_script() {
|
||||
load_remote_conf
|
||||
ssh_remote "REMOTE_DIR='$REMOTE_DIR' REMOTE_PROFILE='$REMOTE_PROFILE' REMOTE_PORT='$REMOTE_PORT' REMOTE_JAVA_OPTS='$REMOTE_JAVA_OPTS' REMOTE_SPRING_ARGS='$REMOTE_SPRING_ARGS' REMOTE_JAVA_BIN='$REMOTE_JAVA_BIN' DEPLOY_NEW='$1' bash -s" <<'EOS'
|
||||
set -euo pipefail
|
||||
cd "$REMOTE_DIR"
|
||||
if [ -f app.pid ] && kill -0 "$(cat app.pid)" 2>/dev/null; then
|
||||
kill "$(cat app.pid)"
|
||||
for i in $(seq 1 15); do kill -0 "$(cat app.pid)" 2>/dev/null || break; sleep 1; done
|
||||
kill -0 "$(cat app.pid)" 2>/dev/null && kill -9 "$(cat app.pid)"
|
||||
fi
|
||||
if [ "$DEPLOY_NEW" = "1" ]; then
|
||||
[ -f xiaoqu-agent-service.jar ] && cp xiaoqu-agent-service.jar "xiaoqu-agent-service.jar.bak.$(date +%Y%m%d%H%M%S)"
|
||||
mv -f xiaoqu-agent-service.jar.new xiaoqu-agent-service.jar
|
||||
fi
|
||||
if [ -f app.env ]; then
|
||||
set -a; source app.env; set +a
|
||||
else
|
||||
echo "[WARN] 未找到 $REMOTE_DIR/app.env,环境变量未注入,prd 可能启动失败" >&2
|
||||
fi
|
||||
nohup "$REMOTE_JAVA_BIN" $REMOTE_JAVA_OPTS -jar xiaoqu-agent-service.jar --spring.profiles.active="$REMOTE_PROFILE" $REMOTE_SPRING_ARGS >> logs/agent-service.log 2>&1 &
|
||||
echo $! > app.pid
|
||||
echo "[OK] 远程已启动 (pid $(cat app.pid)),等待健康检查..."
|
||||
for i in $(seq 1 90); do
|
||||
if ! kill -0 "$(cat app.pid)" 2>/dev/null; then
|
||||
echo "[ERROR] 远程进程已退出,最后 40 行日志:" >&2
|
||||
tail -40 logs/agent-service.log >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "http://127.0.0.1:$REMOTE_PORT/mcp" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"deploy-check","version":"0"}}}' || true)
|
||||
if [ "$code" = "200" ] || [ "$code" = "401" ]; then
|
||||
echo "[OK] 远程服务已就绪 (HTTP $code, /mcp)"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "[ERROR] 远程健康检查超时,日志: $REMOTE_DIR/logs/agent-service.log" >&2
|
||||
exit 1
|
||||
EOS
|
||||
}
|
||||
|
||||
do_remote_deploy() {
|
||||
do_build
|
||||
do_push
|
||||
run_remote_script 1
|
||||
}
|
||||
|
||||
do_remote_restart() {
|
||||
run_remote_script 0
|
||||
}
|
||||
|
||||
do_remote_stop() {
|
||||
load_remote_conf
|
||||
ssh_remote "cd '$REMOTE_DIR' && if [ -f app.pid ] && kill -0 \$(cat app.pid) 2>/dev/null; then kill \$(cat app.pid) && rm -f app.pid && echo '[OK] 远程已停止'; else echo '[SKIP] 远程未在运行'; fi"
|
||||
}
|
||||
|
||||
do_remote_status() {
|
||||
load_remote_conf
|
||||
ssh_remote "cd '$REMOTE_DIR' && if [ -f app.pid ] && kill -0 \$(cat app.pid) 2>/dev/null; then echo \"远程运行中 (pid \$(cat app.pid))\"; else echo '远程未运行'; exit 1; fi"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
build) do_build ;;
|
||||
start) do_start ;;
|
||||
@@ -97,7 +190,13 @@ case "${1:-}" in
|
||||
restart) do_stop; do_start ;;
|
||||
status) do_status ;;
|
||||
log) tail -f "$LOG_FILE" ;;
|
||||
push) do_push ;;
|
||||
remote-deploy) do_remote_deploy ;;
|
||||
remote-restart) do_remote_restart ;;
|
||||
remote-stop) do_remote_stop ;;
|
||||
remote-status) do_remote_status ;;
|
||||
remote-log) load_remote_conf; ssh_remote "tail -f '$REMOTE_DIR/logs/agent-service.log'" ;;
|
||||
*)
|
||||
echo "用法: $0 {build|start|stop|restart|status|log}" >&2
|
||||
echo "用法: $0 {build|start|stop|restart|status|log|push|remote-deploy|remote-restart|remote-stop|remote-status|remote-log}" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# 远程部署配置(本文件为模板;实际使用复制为 remote.conf,已 gitignore,不入库)
|
||||
# 用法: cp deploy/remote.conf.example deploy/remote.conf,填好后执行 ./deploy/deploy.sh remote-deploy
|
||||
|
||||
# --- SSH 连接(必填)---
|
||||
# 推荐先在 ~/.ssh/config 配置别名和密钥,这里直接填别名,避免明文密码
|
||||
# 例如: REMOTE_SSH_HOST=xiaoqu-prd 或 REMOTE_SSH_HOST=deploy@192.168.1.100
|
||||
REMOTE_SSH_HOST=
|
||||
REMOTE_SSH_PORT=22
|
||||
|
||||
# --- 远程目录(必填)---
|
||||
# 远程服务器上的部署目录,jar、pid、日志都放在这里
|
||||
REMOTE_DIR=/opt/xiaoqu/agent-service
|
||||
|
||||
# --- 运行参数 ---
|
||||
# Spring profile(对应 application-<profile>.yml;若远程没有对应 yml,
|
||||
# 用下面的 REMOTE_SPRING_ARGS 覆盖具体配置项)
|
||||
REMOTE_PROFILE=prod
|
||||
# 服务端口(健康检查用)
|
||||
REMOTE_PORT=8088
|
||||
|
||||
# JVM 参数(可选)
|
||||
REMOTE_JAVA_OPTS="-Xms512m -Xmx1024m"
|
||||
|
||||
# java 命令路径(可选,默认用 PATH 里的 java;远程有多个 JDK 时建议显式指定 17)
|
||||
# REMOTE_JAVA_BIN=/usr/lib/jvm/jre-17-openjdk/bin/java
|
||||
REMOTE_JAVA_BIN=
|
||||
|
||||
# 追加给 Spring Boot 的启动参数(可选),用于覆盖远程环境配置,例如:
|
||||
# REMOTE_SPRING_ARGS=--spring.datasource.url=jdbc:mysql://xxx:3306/db --spring.datasource.password=xxx
|
||||
REMOTE_SPRING_ARGS=
|
||||
|
||||
# 远程服务器要求: JDK 17+、curl(健康检查用)
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# 本机即测试环境:一键 构建 → 重启 → 刷新 scope 工具绑定 → 自检
|
||||
# 用法:
|
||||
# ./deploy/test-up.sh # 全量(含 Maven 构建,约 7 分钟)
|
||||
# ./deploy/test-up.sh --skip-build # 跳过构建,直接重启+刷新绑定
|
||||
# 首次运行会自动生成 deploy/.env.dev(含随机 admin 初始密码,注意看输出)
|
||||
set -euo pipefail
|
||||
|
||||
APP_HOME="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DEPLOY="$APP_HOME/deploy/deploy.sh"
|
||||
ENV_FILE="$APP_HOME/deploy/.env.dev"
|
||||
SEED_SQL="$APP_HOME/sql/seed-dev.sql"
|
||||
MYSQL="${MYSQL_BIN:-mysql}"
|
||||
|
||||
# dev svc 库(与 application-dev.yml 保持一致)
|
||||
SVC_DB_HOST=192.168.1.181
|
||||
SVC_DB_USER=root
|
||||
SVC_DB_PASSWORD=kaixinjiuhao
|
||||
SVC_DB_NAME=xiaoqu_agent_svc_d
|
||||
|
||||
# 1. 首启引导配置(不存在才生成;admin 仅首次启动且 admin_user 为空时建号)
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
ADMIN_PWD="Dev@$(openssl rand -hex 6)"
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
# dev 环境变量(本机使用,已 gitignore)
|
||||
ADMIN_BOOTSTRAP_ACCOUNT=admin
|
||||
ADMIN_BOOTSTRAP_PASSWORD=$ADMIN_PWD
|
||||
ADMIN_BOOTSTRAP_NAME=系统管理员
|
||||
SVC_FLYWAY_BASELINE_ON_MIGRATE=false
|
||||
EOF
|
||||
echo "[INIT] 已生成 $ENV_FILE"
|
||||
echo "[INIT] admin 初始密码: $ADMIN_PWD (仅首次启动建号生效,请记录)"
|
||||
fi
|
||||
|
||||
# 2. 构建
|
||||
if [ "${1:-}" = "--skip-build" ]; then
|
||||
echo "[SKIP] 跳过构建"
|
||||
else
|
||||
"$DEPLOY" build
|
||||
fi
|
||||
|
||||
# 3. 重启(deploy.sh 内含 /mcp 健康检查)
|
||||
"$DEPLOY" restart
|
||||
|
||||
# 4. 刷新 scope-工具绑定(tool 表已由启动时 ToolSyncService 同步;seed 幂等)
|
||||
"$MYSQL" -h "$SVC_DB_HOST" -u"$SVC_DB_USER" -p"$SVC_DB_PASSWORD" "$SVC_DB_NAME" \
|
||||
--default-character-set=utf8mb4 < "$SEED_SQL" > /dev/null
|
||||
echo "[OK] scope 工具绑定已刷新(operation)"
|
||||
|
||||
# 5. 自检:后台登录接口存活(不验密码对错,只验服务可登录)
|
||||
code=$(curl -s --noproxy '*' -o /dev/null -w "%{http_code}" -m 10 -X POST \
|
||||
http://127.0.0.1:8088/api/admin/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"account":"__selfcheck__","password":"__selfcheck__"}')
|
||||
if [ "$code" = "401" ] || [ "$code" = "200" ]; then
|
||||
echo "[OK] 登录接口存活 (HTTP $code)"
|
||||
else
|
||||
echo "[WARN] 登录接口异常 (HTTP $code),请查看: $DEPLOY log" >&2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "本机测试环境就绪: http://localhost:8088/ (admin 账号见 deploy/.env.dev)"
|
||||
echo "日常命令: $DEPLOY {start|stop|restart|status|log}"
|
||||
@@ -106,14 +106,14 @@ xiaoqu-ai Android/iOS/Web ─► Gateway/Agent ─────┤
|
||||
|
||||
### 5.1 MCP(主形态)
|
||||
|
||||
- Streamable HTTP 传输,端点 `https://mcp.<域名>/mcp`。
|
||||
- Streamable HTTP 传输,端点 `https://agent.ququclean.com/mcp`(生产,2026-09-11 上线,Nginx 终结 TLS)。
|
||||
- 客户端配置(Kimi Code `~/.kimi-code/mcp.json` 或项目级 `.kimi-code/mcp.json`,ak 明文直写;项目级文件已加入 `.gitignore`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"xiaoqu": {
|
||||
"url": "https://mcp.<域名>/mcp",
|
||||
"url": "https://agent.ququclean.com/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer <ak>"
|
||||
}
|
||||
@@ -122,7 +122,7 @@ xiaoqu-ai Android/iOS/Web ─► Gateway/Agent ─────┤
|
||||
}
|
||||
```
|
||||
|
||||
- Claude Code:`claude mcp add --transport http xiaoqu https://mcp.<域名>/mcp --header "Authorization: Bearer <ak>"`。
|
||||
- Claude Code:`claude mcp add --transport http xiaoqu https://agent.ququclean.com/mcp --header "Authorization: Bearer <ak>"`。
|
||||
- ak 明文只出现在服务端签发/ensure 响应和运行时;服务端保存 SHA-256 哈希及 AES-GCM 加密密文(二次获取用),含 ak 的客户端配置文件不得提交版本库。
|
||||
|
||||
### 5.2 Web 管理后台(四个页面)
|
||||
@@ -151,7 +151,7 @@ xiaoqu-ai Android/iOS/Web ─► Gateway/Agent ─────┤
|
||||
|
||||
供其他业务系统/智能体平台在服务端到服务端场景下获取用户 Token,路径 `/api/open/**`,与 ak 体系、后台 session 体系完全分离。
|
||||
|
||||
- **鉴权**:静态服务间密钥,header `X-Open-Key`,配置项 `app.open.api-key`(dev 明文写 `application-dev.yml`;配置为空 = 接口关闭)。常量时间比较防时序侧信道。
|
||||
- **鉴权**:服务间密钥放在 header `X-Open-Key`。生产密钥由现有管理后台创建,数据库只存 SHA-256 哈希并绑定作用域;旧 `app.open.api-key` / `app.open.clients` 配置保留兼容,其中环境变量明文使用常量时间比较。
|
||||
- **`POST /api/open/token/ensure`**(get-or-create):
|
||||
- 入参:`mobile`、`email`(至少一个,手机号优先)、`scopeCode`(作用域编码,必填)。
|
||||
- 人员只认企业账号(`enterprise_type` 0/1,NULL 保留,与后台用户列表同口径);查不到 → 404。
|
||||
@@ -159,6 +159,15 @@ xiaoqu-ai Android/iOS/Web ─► Gateway/Agent ─────┤
|
||||
- 已存在但**无密文**(V3 之前的历史数据)→ 自动轮换:禁用旧把、签发新把,按 `created=true` 返回新明文;解密失败 → 500,不回退。
|
||||
- **不存在** → 自动创建永久 Token(一作用域一 Token,memo 标记 `open-api 自动签发`),`created=true` 返回明文。
|
||||
- 全程写 `audit_log`(`tool_name=open.token.ensure`,`client_type=open-api`,参数摘要脱敏,不记录明文)。
|
||||
- **服务 Token(principal_type=service,REQ-20260911-0014)**:
|
||||
- `agent_api_key.principal_type` 区分 `user`(用户委托,绑定 `user_id`,行级权限跟随人员)与 `service`(租户服务,`user_id` 为 NULL,`subject_name` 标识服务主体,行级权限不适用,数据范围 = 绑定作用域全量)。
|
||||
- ensure 入参补 `principalType` / `serviceName`:`principalType=service` 时按 `serviceName + scopeCode` get-or-create 服务 Token,语义与 user 路径一致(存在解密回传、无密文自动轮换、不存在签发永久 Token)。
|
||||
- **服务密钥 → 数据域绑定**:管理后台「服务密钥」页面创建调用方并勾选作用域,鉴权时按密钥确定可操作的数据域,越域 403;密钥明文仅在创建/轮换时展示,吊销/轮换立即生效。`app.open.clients` 作为环境变量兼容通道;旧 `app.open.api-key` 保留为不限域密钥,仅存量 user ensure 可用,**不可签发 service Token**。
|
||||
- 吊销:后台 `PATCH /api/admin/tokens/{id}/status` 置 0,`AkAuthFilter` 现有 status 校验天然生效,吊销后立即 401;轮换:`POST /api/admin/tokens/{id}/rotate` 禁用旧把并按原主体/作用域/过期时间签新把,明文仅本次响应返回。
|
||||
- **人员停用失效**:`AuthService` 校验 user 类型 Token 时检查 m0.user `soft_delete`(注意 `work_status` 是考勤状态,不是在职状态),人员状态本地缓存 TTL 60s(→ Redis 热缓存 → m0 库三级),停用最长 60s 后 ak 调用返回 401/UNAUTHENTICATED。
|
||||
- **SYNC 工具白名单**:个人 Token 禁调 SYNC 工具不新造拦截代码,靠 `token_scope` 白名单——SYNC 工具(`*.changes.list`,后续需求 5628 注册)只绑进 `sync` 作用域,个人 Token 不勾该作用域即天然 PERMISSION_DENIED;`sync` 作用域由 `sql/seed-dev.sql` 预建。ensure 侧同时做入口约束:`principalType=service` 只允许 `scopeCode=sync`,user ensure 不允许 `sync`。
|
||||
- **手机号变更调研结论(REQ-20260911-0014 范围 5)**:`mobile` 是 `m0.user` 的普通属性,变更手机号不改 `user.id`;Token 与行级权限均按 `user.id` 生效,因此手机号变更后仍保持同一个 userId,无需额外处理。
|
||||
- **5629 冒烟清单**:① scoped key 跨域 ensure → 403;② 旧 `app.open.api-key` 签发 service → 403;③ `principalType=service` + `scopeCode=sync` ensure 成功,拿 ak 调 `integration.context.get` 返回 `principal_type=tenant_service`;④ 后台吊销该 Token 后立即 401;⑤ rotate 后旧 ak 401、新明文可用;⑥ 人员停用后等待 >60s(本地缓存 TTL)再调 → 401/UNAUTHENTICATED;⑦ user ensure `scopeCode=sync` → 403。
|
||||
|
||||
## 6. 技术栈与工程结构
|
||||
|
||||
@@ -194,6 +203,8 @@ xiaoqu-agent-service/
|
||||
| `tool` | name、category、description、kind(read/write)、status | 工具注册表,启动时同步;category 代码声明 |
|
||||
| `audit_log` | user_id、token_id、tool_name、params_digest、result_status、cost_ms、client_ip、client_type | 审计;client_type 区分 mcp-kimi/mcp-claude/cli |
|
||||
| `admin_user` | account、password_hash | 后台管理员 |
|
||||
| `app_open_client` | name、key_prefix、key_hash、status、last_used_time | 管理后台创建的 Open API 服务密钥调用方(不存明文) |
|
||||
| `app_open_client_scope` | client_id、scope_id | 服务密钥-作用域关系 |
|
||||
|
||||
均带标准审计五件套(create/modify id/time、soft_delete)。
|
||||
|
||||
@@ -211,7 +222,7 @@ xiaoqu-agent-service/
|
||||
| P1 骨架 | 工程初始化、版本组合验证、三数据源连通、codegen 跑通 | demo 工具经 MCP 调通 |
|
||||
| P2 权限 | ak 鉴权 Filter、scope 校验、数据权限引擎、审计 | 不同权限组的人查询结果隔离正确 |
|
||||
| P3 首批工具 | 代理商查询 + 客户(物业)查询(定义见 §9.1)+ tool 表同步 | Kimi/Claude 双端实测 |
|
||||
| P4 管理后台 | 用户/token、工具、作用域、审计四页面(Vue3 内嵌) | 管理员全流程自助配置 |
|
||||
| P4 管理后台 | 用户/token、服务密钥、工具、作用域、审计五页面(Vue3 内嵌) | 管理员全流程自助配置 |
|
||||
| P5 加固 | 限流、ES 接入(按需)、写工具评审开放、CLI(Go 单二进制) | 生产上线 |
|
||||
|
||||
### 9.1 首期工具定义(P3,均已确认)
|
||||
|
||||
@@ -13,6 +13,40 @@
|
||||
| 行级数据权限 | 跟 ak 所属人走,照搬老系统 `getByIsAgent` 语义(见设计文档 §3.3),与作用域无关 |
|
||||
| 分页 | 列表类工具统一 `page`(从 1 开始,默认 1)、`rows`(默认 20,**上限 100**,超出截断为 100 并在返回 `notice` 字段提示) |
|
||||
| 分页出参 | 统一含 `total`(总数)、`page`、`rows`、`records`(本页记录)、`notice`(仅截断时出现) |
|
||||
| 工具声明 | `tools/list` 中每个工具带 MCP annotations:只读(`readOnlyHint=true`)、幂等(`idempotentHint=true`)、非破坏性(`destructiveHint=false`)(REQ-20260911-0011 起) |
|
||||
|
||||
### 0.1 schema_version 双跑(REQ-20260911-0011 起)
|
||||
|
||||
- **默认(v1,兼容面)**:`tools/call` 返回的 `result.content[0].text` 即各工具原有 JSON,无 `structuredContent`,老客户端零改动。
|
||||
- **显式声明新版本**:请求头加 `X-Mcp-Schema-Version: 2`,`tools/call` 响应额外写入 `result.structuredContent` 统一信封,`content` 文本保持不变:
|
||||
|
||||
```json
|
||||
{ "schema_version": 2, "request_id": "<服务端生成 UUID>", "data": { "…": "各工具原有 JSON" } }
|
||||
```
|
||||
|
||||
- 版本头只接受 `1`/`2`(缺省按 1);其他值在 `tools/call` 上直接返回 `SCHEMA_VERSION_UNSUPPORTED` 错误,不执行工具。
|
||||
- v2 请求下 `tools/list` 的每个工具额外带 `outputSchema`(信封结构);业务字段契约仍以本文档各工具章节为准。
|
||||
|
||||
### 0.2 统一错误码(REQ-20260911-0011 起)
|
||||
|
||||
工具执行失败返回 `isError=true`,`content[0].text` 为结构化 JSON(不再是裸异常文本);v2 下同样的错误出现在 `structuredContent.error`:
|
||||
|
||||
```json
|
||||
{ "error": { "code": "PERMISSION_DENIED", "message": "人类可读说明", "request_id": "…" } }
|
||||
```
|
||||
|
||||
| code | 含义 |
|
||||
|---|---|
|
||||
| UNAUTHENTICATED | 缺少有效 ak 或鉴权上下文丢失 |
|
||||
| PERMISSION_DENIED | token 作用域不含该工具或工具已停用 |
|
||||
| NOT_FOUND_OR_FORBIDDEN | 资源不存在或无权查看(不区分,防探测) |
|
||||
| RATE_LIMITED | 触发限流(预留) |
|
||||
| UPSTREAM_UNAVAILABLE | 上游数据源(DB/Redis)暂时不可用 |
|
||||
| SYNC_CURSOR_EXPIRED | 增量同步游标过期(预留) |
|
||||
| SCHEMA_VERSION_UNSUPPORTED | 声明的 schema_version 不受支持 |
|
||||
| DATA_INTEGRITY_ERROR | 数据不一致或未预期异常(兜底;细节只落服务端日志,凭 request_id 排查) |
|
||||
|
||||
日志与审计不记录 Token/ak,手机号一律脱敏为 `138****5678` 形式。
|
||||
|
||||
---
|
||||
|
||||
@@ -119,7 +153,66 @@
|
||||
|
||||
---
|
||||
|
||||
## 4. sys.ping
|
||||
## 4. district.list.query
|
||||
|
||||
分页查询站点列表,对应老系统 `/district/districtlistByPage`,但使用固定轻量字段契约。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m0 库 `district_district_strategy` 视图,固定 `soft_delete = 0`、`id DESC`
|
||||
- 行级权限:总部看全部;代理商按自己、直属下级、代管代理及授权物业过滤;销售部门非负责人沿用老接口的互斥物业范围。筛选条件只能缩小权限结果。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | number | 否 | 站点 id,正整数,精确匹配 |
|
||||
| districtName | string | 否 | 站点名称,模糊匹配 |
|
||||
| districtState | number | 否 | 0=测试 1=履约 2=演示 3=撤机 |
|
||||
| province | string | 否 | 省份,模糊匹配 |
|
||||
| city | string | 否 | 城市,模糊匹配 |
|
||||
| agentId | number | 否 | 代理商 id,正整数,精确匹配 |
|
||||
| propertyId | number | 否 | 物业 id,正整数,精确匹配 |
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| rows | number | 否 | 每页条数,默认 20,上限 100 |
|
||||
|
||||
**出参 records 字段**:
|
||||
|
||||
`id`、`districtName`、`districtNumber`、`propertyId`、`propertyName`、`agentId`、`agentName`、`province`、`city`、`countyseat`、`address`、`districtState`、`onlineState`、`hardwareCount`、`isSmart`。
|
||||
|
||||
`propertyName`、`agentName` 均为批量富化,查不到时返回 `未查询到`。
|
||||
|
||||
---
|
||||
|
||||
## 5. district.detail.get
|
||||
|
||||
按站点 id 获取扩展详情。站点 id 与当前用户行权限在同一个数据库查询条件中组合。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 入参:`id`(number,必填,正整数)
|
||||
- 找到:`{ "found": true, "data": { ... } }`
|
||||
- 不存在或无权限:`{ "found": false, "message": "站点不存在或无权访问" }`
|
||||
|
||||
**data 字段**:
|
||||
|
||||
| 分组 | 字段 |
|
||||
|---|---|
|
||||
| 归属 | id、districtName、districtNumber、propertyId、propertyName、agentId、agentName、salesId、salesName、serviceCompanyId、serviceCompany |
|
||||
| 地址联系人 | province、city、countyseat、address、contact、phone、mail |
|
||||
| 站点属性 | businessFormat、attributeId、attributeName、installType、isSmart、lnfluenceLevel、buildCount、hardwareCount、airdetectorFlag |
|
||||
| 状态周期 | districtState、onlineState、testDate、honourDate、weaningDate、testDays |
|
||||
| 设备耗材 | deviceSalesType、paperSalesType、consumablesType、posDevicePaperNorm、toiletDevicePaperNorm |
|
||||
| 功能开关 | pointState、assignmentWorkState、airState、messageFlag、reportFlag、lockMachineFlag、eventRepairFlag、rfidUseFlag、calibratedFlag、illegalPaperDisable |
|
||||
| 语音配置 | lowerPaperState、noPaperState、powerWarning、waitTimes |
|
||||
| 报警配置 | warning、warning2、warning3、warningTime、paperPercentWarn、maintainOrderWarning、foamCountWarning、foamPowerWarning |
|
||||
| 业务配置 | pointDeduction、effectiveUseTime、consumablesPrice、billDate、billType、billTypeStr、fineMoney |
|
||||
| 运行配置 | districtRefresh(Redis 未配置默认 5)、forbidFunction(Redis 未配置默认 0) |
|
||||
| 审计信息 | createId、createName、createTime、modifyTime、memo |
|
||||
|
||||
固定不返回 `softDelete`、`enterpriseId`、`modifyId`、内部服务 URL、重复 `districtId`、语义未确认的 `supervisorId` 和 SKU 列表。
|
||||
|
||||
---
|
||||
|
||||
## 6. sys.ping
|
||||
|
||||
服务自检(demo 工具,证明链路连通)。
|
||||
|
||||
@@ -138,6 +231,358 @@
|
||||
|
||||
---
|
||||
|
||||
## 7. mall.order.list.query
|
||||
|
||||
查询商城订单列表(分页),对应老系统 `POST /mallGoodsOrder/listByPage`。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m1 商城库(`xiaoqu_mall_d`)`mall_goods_order` 表
|
||||
- 行级权限(`PermissionService.applyOrderRowFilter`,移植老接口 `WrapperUtils.getSaleCompany` 语义):
|
||||
- 总部(isagent=0):全部;
|
||||
- 代理商(isagent=1):`sale_company_id IN (自己 + 直属下级 + warehouse_agent_auth 代管代理)`,有代管物业授权时再 `OR property_id IN (agent_manage_district_auth 代管物业)`;
|
||||
- 销售部门非负责人:`property_id IN (自己创建的物业 + 代管代理名下物业)`,与上一条互斥。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| orderNo | string | 否 | 订单编号,模糊匹配 |
|
||||
| agentId | number | 否 | 代理商 id,精确匹配 |
|
||||
| propertyId | number | 否 | 物业 id,精确匹配 |
|
||||
| orderType | number | 否 | 订单类型:0=代理 1=物业 |
|
||||
| orderState | number | 否 | 订单状态(单选):0=待付款 1=待出库 2=待发货 3=已发货 4=已签收 5=已收货 6=已取消 7=已删除 |
|
||||
| payState | number | 否 | 回款状态:0=待付款 1=已付款 2=付款中 |
|
||||
| startOrderTime | string | 否 | 下单时间起,`yyyy-MM-dd` |
|
||||
| endOrderTime | string | 否 | 下单时间止,`yyyy-MM-dd`(含当天) |
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| rows | number | 否 | 每页条数,默认 20,上限 100(超出截断为 100 并在 notice 提示) |
|
||||
|
||||
**出参 records 字段**(按 id 倒序):
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| id / orderNo / orderTime | 订单 id / 编号 / 下单时间(`yyyy-MM-dd HH:mm:ss`) |
|
||||
| orderType / agentName / propertyName | 订单类型 / 代理名称 / 物业名称 |
|
||||
| saleCompany | 销售公司 |
|
||||
| payMoney / paidMoney / unPayMoney | 应付 / 已付 / 未付金额 |
|
||||
| orderState / payState / warehouseState | 订单状态 / 回款状态 / 出库状态(0=未出库 1=已出库 2=部分出库) |
|
||||
| customerName | 客户名称:orderType=0 取代理金蝶名称,否则取物业金蝶名称(批量富化,同老接口) |
|
||||
|
||||
---
|
||||
|
||||
## 8. mall.order.detail.query
|
||||
|
||||
按订单号查询商城订单详情(订单 + 商品明细 + 收货地址一次返回,合并老系统三个接口)。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m1 库 `mall_goods_order` + `mall_order_details` + `mall_shipping_address`
|
||||
- 行级权限:与 `mall.order.list.query` 一致;订单不存在或无权查看时 `record=null` 并在 notice 说明(两者不区分,不泄露订单是否存在)。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| orderNo | string | 是 | 订单编号 |
|
||||
|
||||
**出参**:
|
||||
|
||||
- `record`:订单关键字段(金额类 payMoney/paidMoney/unPayMoney/totalMoney/goodsMoney/postageMoney/discountMoney,状态类 orderState/payState/orderPayState/warehouseState/orderInvoiceState,时间类 orderTime/payTime/receiptTime/signTime/createTime,及 accountsType/paymentType/goodsType/saleCompany/payNo/memo 等)+ 收货信息(reveiveName/reveiveNumber/province/city/countyseat/address,由 `address_id` 关联 `mall_shipping_address`)。
|
||||
- `items`:商品明细列表,每行含 spuNo/spuName/skuNo/count/price/payMoney/payState/warehouseState。
|
||||
|
||||
---
|
||||
|
||||
## 9. mall.order.logistics.query
|
||||
|
||||
按订单号查询商城订单物流(发货信息 + 物流轨迹),对应老系统 `GET /mallGoodsOrder/getMallLogistics`。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m1 库 `mall_order_logistics` + `mall_order_logistics_detai`
|
||||
- 行级权限:与 `mall.order.list.query` 一致;订单不存在或无权查看时 `logistics=null` 并在 notice 说明(两者不区分)。
|
||||
- **注意**:轨迹表 `logistics_order_no` 字段老系统写入时塞的是订单号而非快递单号,故按 orderNo 查询(照搬老系统语义)。物流轨迹为系统内业务节点手工写入的记录,非实时快递查询,可能不全(菜鸟定时同步已停用)。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| orderNo | string | 是 | 订单编号 |
|
||||
|
||||
**出参**:
|
||||
|
||||
- `logistics`(无物流记录时为 null):logisticsName(快递公司)/ logisticsOrderNo(快递单号)/ sendMode(0=快递 1=自送)/ state(0=已出单 1=已出库 2=已发货 3=已揽件 4=运输中 5=待取件 6=派送中 7=已签收)/ deliverName / deliverNumber / sendTime / receivingGoodsName / reveiveName / reveiveNumber / address。
|
||||
- `tracks`:轨迹列表,按时间正序,每行含 memo(轨迹描述)/ state / picList / createTime。
|
||||
|
||||
---
|
||||
|
||||
## 10. hardware.list.query
|
||||
|
||||
分页查询出纸机列表,对应老系统 `POST /hardware/listByPage`(HardwareViewController)。
|
||||
|
||||
- 分类:hardware | 类型:读
|
||||
- 数据来源:m0 库 `hardware_view` 视图,固定 `soft_delete = 0`、`id DESC`
|
||||
- 行级权限(`PermissionService.applyHardwareRowFilter`,与站点同一 `getByIsAgent(propertyFlag=true, className="property_id")` 变体):
|
||||
- 总部(isagent=0):全部;
|
||||
- 代理商(isagent=1):`agent_id IN (自己 + 直属下级 + warehouse_agent_auth 代管代理)`,有代管物业授权时再 `OR property_id IN (agent_manage_district_auth 代管物业)`;
|
||||
- 销售部门非负责人:`property_id IN (自己创建的物业 + 代管代理名下物业)`,与上一条互斥。
|
||||
- 富化(修正老接口 N+1,按页批量):`agentName`/`propertyName`/`districtName` 走 Redis key `name:Agent:{id}` / `name:Properyt:{id}` / `name:District:{id}` multiGet;断纸状态 `piStatus` 批量读 `Assignment:{snCode}:29`(无 key 返回 0);本页非空 `imsi` 一次性 IN 查 `networkcard` 表。返回 `hardware_view` 全字段,不做字段级权限裁剪。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| snCode | string | 否 | sn码,模糊匹配 |
|
||||
| deviceType | number | 否 | 设备类型:0=厕纸机 1=擦手纸机 |
|
||||
| status | number | 否 | 设备状态:1=在线 2=故障 3=离线 |
|
||||
| online | number | 否 | 上下线状态:1=上线 2=下线 |
|
||||
| lockMachineState | number | 否 | 锁机状态:0=未锁机 1=已锁机 |
|
||||
| agentId | number | 否 | 代理商 id,正整数,精确匹配 |
|
||||
| propertyId | number | 否 | 物业 id,正整数,精确匹配 |
|
||||
| districtId | number | 否 | 站点 id,正整数,精确匹配 |
|
||||
| buildingId | number | 否 | 楼 id,正整数,精确匹配 |
|
||||
| floor | string | 否 | 楼层,精确匹配 |
|
||||
| imsi | string | 否 | imsi码,精确匹配 |
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| rows | number | 否 | 每页条数,默认 20,上限 100(超出截断为 100 并在 notice 提示) |
|
||||
|
||||
**出参 records 字段**:`hardware_view` 全字段(snCode、deviceType、status/online/lockMachineState 状态四元组、sysversion/firmwareVersion/voiceVersion/advertiseVersion、imsi、电量/纸量/电池族、各功能开关、memo 等,时间格式 `yyyy-MM-dd HH:mm:ss`)+ 富化字段 `agentName`/`propertyName`/`districtName`/`piStatus` + 物联网卡字段 `factoryType`/`carrier`/`dataBalance`/`dataUsage`/`iccid`/`msisdn`(按 imsi 匹配,无卡为 null)。
|
||||
|
||||
---
|
||||
|
||||
## 11. hardware.detail.query
|
||||
|
||||
按 snCode 或 id 查询出纸机详情(二选一必填),行级权限与 `hardware.list.query` 一致。
|
||||
|
||||
- 分类:hardware | 类型:读
|
||||
- 数据来源:m0 库 `hardware_view` + `district_district_strategy`(站点信息)+ `networkcard`(物联网卡)
|
||||
- 设备不存在或无权查看时 `record=null` 并在 notice 说明(两者不区分,不泄露设备是否存在)。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| snCode | string | 条件必填 | sn码,精确匹配;与 id 二选一 |
|
||||
| id | number | 条件必填 | 设备 id,正整数,精确匹配;与 snCode 二选一 |
|
||||
|
||||
**出参**:
|
||||
|
||||
- `record`:hardware_view 全字段 + 与列表同一套富化(agentName/propertyName/districtName、piStatus、物联网卡 factoryType/carrier/dataBalance/dataUsage/iccid/msisdn)。
|
||||
- `record.site`(关联站点,district 表现有字段):`districtId`、`districtName`、`province`、`city`、`countyseat`、`address`、`contact`、`phone`、`buildingId`、`floor`。注意 district 表无经纬度和楼名字段,楼座/楼层取 hardware_view。
|
||||
|
||||
---
|
||||
|
||||
## 12. contract.list.query
|
||||
|
||||
分页查询合同列表,对应老系统 `/contract/listByPage` 合同列表页(权限库 `webpage` id=394 `contractList`,页面字段定义 `ContractListOutput`)。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m2 库 `contract_new` 表,固定 `soft_delete = 0`、`id DESC`
|
||||
- 行级权限(`PermissionService.applyContractRowFilter`,与站点/出纸机同一 `getByIsAgent(propertyFlag=true)` 变体,agent 维度落 `contract_agent_id`,property 维度落 `property_id`):
|
||||
- 总部(isagent=0):全部;
|
||||
- 代理商(isagent=1):`contract_agent_id IN (自己 + 直属下级 + warehouse_agent_auth 代管代理)`,有代管物业授权时再 `OR property_id IN (agent_manage_district_auth 代管物业)`;
|
||||
- 销售部门非负责人:`property_id IN (自己创建的物业 + 代管代理名下物业)`,与上一条互斥。
|
||||
- 富化(按页批量 IN 查询,消除 N+1):`contractAgentName`/`customerAgentName` 查 m0 `agent`,`createName` 查 m0 `user`;`saleName`/`propertyName`/`customerName`/`customerNo` 为合同表冗余快照,直接返回。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| contractNo | string | 否 | 合同编号,模糊匹配 |
|
||||
| contractName | string | 否 | 合同名称,模糊匹配 |
|
||||
| propertyName | string | 否 | 物业名称,模糊匹配 |
|
||||
| customerName | string | 否 | 客户名称(金蝶客户名称快照),模糊匹配 |
|
||||
| customerNo | string | 否 | 客户编号(=物业 serialnumber),精确匹配 |
|
||||
| contractAgentId | number | 否 | 合同所属公司 id,精确匹配 |
|
||||
| customerAgentId | number | 否 | 客户所属公司 id,精确匹配 |
|
||||
| propertyId | number | 否 | 所属物业 id,精确匹配 |
|
||||
| contractStatus | number | 否 | 合同状态:0=待审 1=驳回 2=正常 3=过期 4=终止 |
|
||||
| expireRemind | number | 否 | 到期提醒开关:0=关 1=开 |
|
||||
| endDateFrom | string | 否 | 合同结束日期起,格式 yyyy-MM-dd(查即将到期合同用) |
|
||||
| endDateTo | string | 否 | 合同结束日期止,格式 yyyy-MM-dd(含当天) |
|
||||
| page | number | 否 | 页码,从 1 开始,默认 1 |
|
||||
| rows | number | 否 | 每页条数,默认 20,上限 100(超出截断为 100 并在 notice 提示) |
|
||||
|
||||
**出参 records 字段**:`contract_new` 主要字段(id、contractNo、contractName、contractAgentId/customerAgentId、propertyId/propertyName、customerNo/customerName、amount、contact、startDate/endDate(`yyyy-MM-dd`)、expireRemind/remindDays/remindTime/reminded、saleId/saleName、contractStatus、stopReasonType/stopReason、createId、createTime)+ 富化字段 `contractAgentName`/`customerAgentName`/`createName` + 状态文案 `contractStatusText`。
|
||||
|
||||
---
|
||||
|
||||
## 13. contract.detail.query
|
||||
|
||||
按 id 或 contractNo 查询合同详情(二选一必填),行级权限与 `contract.list.query` 一致。
|
||||
|
||||
- 分类:customer | 类型:读
|
||||
- 数据来源:m2 库 `contract_new` + `contract_file`(附件)+ `contract_process`(流程/待办/操作记录)
|
||||
- 合同不存在或无权查看时 `record=null` 并在 notice 说明(两者不区分,不泄露合同是否存在);id/contractNo 都未传时 notice 提示"必须传一个"。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | number | 条件必填 | 合同 id;与 contractNo 二选一 |
|
||||
| contractNo | string | 条件必填 | 合同编号,精确匹配;与 id 二选一 |
|
||||
|
||||
**出参**:
|
||||
|
||||
- `record`:与列表同一套字段(含富化和 `contractStatusText`)。
|
||||
- `files`:合同附件列表,`id`/`fileName`/`fileUrl`/`createTime`,按 id 正序。
|
||||
- `processes`:审批流程记录,按 id 正序,字段 `nodeType`(0=创建 1=部门内审核 2=层级审核 3=终止 4=抄送)、`auditLevel`、`operationType`(0=创建 1=审核通过 2=审核驳回 3=终止 4=修改再提交 5=抄送)、`userId`/`userName`、`scheduleState`(0=无需待办 1=待办 2=已办)、`remark`、`operateTime`,均附 `nodeTypeText`/`operationTypeText`/`scheduleStateText` 文案。
|
||||
|
||||
---
|
||||
|
||||
## 14. integration.context.get
|
||||
|
||||
身份与能力自检(REQ-20260911-0012):返回当前 Token 的集成上下文。所有数据每次调用实时取库,不缓存权限。
|
||||
|
||||
- 分类:integration | 类型:读
|
||||
- 数据来源:svc 库 `agent_api_key` / `token_scope` / `agent_scope` + ToolGuard 实时能力计算(与 tools/call 强制校验同源)
|
||||
|
||||
**入参**:无
|
||||
|
||||
**出参**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| tenant_id | string | Token 所属数据域标识 = 启用状态的作用域编码(排序后逗号拼接;当前签发规则为一作用域一 Token) |
|
||||
| principal_type | string | `user_delegated`(用户委托)/ `tenant_service`(服务主体)。当前按 token 绑定用户推导;`agent_api_key.principal_type` 列由后续需求(5629)补充后切换为读列 |
|
||||
| subject_id | number | 用户 ID 或服务主体 ID |
|
||||
| scopes | string[] | 该 Token 允许调用的工具名集合(排序),与 tools/call 强制校验结果严格一致 |
|
||||
| data_scope_version | number | 数据权限版本:token_scope 绑定集合最大 modify_time(空取 create_time)的 epoch 毫秒;无绑定为 0 |
|
||||
| capability_levels | string[] | `QUERY` / `SYNC`:能力集合中含 `*.changes.list` 工具时具备 SYNC(数据驱动);无能力时为空数组 |
|
||||
| server_time | string | 带时区的服务器时间(ISO_OFFSET_DATE_TIME) |
|
||||
|
||||
---
|
||||
|
||||
## 15. warehouse.out.list.query
|
||||
|
||||
分页查询出库单,参考旧系统 `POST /myWarehouseOut/allListByPage`,但修复其出库日期误映射为申请日期、
|
||||
部分渠道未过滤和逐单查询明细的问题。
|
||||
|
||||
- 分类:warehouse | 类型:读
|
||||
- 数据来源:m2 `warehouse_out`、`warehouse_out_details`、`warehouse_out_audit_process`
|
||||
- 行级权限:总部看全部;代理身份可看主单所属代理为自己/直属下级/授权代管代理的出库单,或明细涉及
|
||||
上述代理所属/负责管理仓库的出库单,或本人参与审核/处理的出库单。三类条件取并集;筛选条件只能缩小结果。
|
||||
- 列表关联数据按本页主单 ID 批量查询,不逐单 N+1 查询。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| outOrderNo / allocationOrderNo / mallOrderNo | string | 否 | 出库/调拨/商城单号,模糊匹配 |
|
||||
| deliveryOrderNo / installOrderNo / pickupOrderNo | string | 否 | 送货/装机/取货工单号,模糊匹配 |
|
||||
| agentId / warehouseId / warehouseTypeId / applyUserId | number | 否 | 所属代理/涉及仓库/出库类型/申请人 ID,精确匹配 |
|
||||
| orderState | number | 否 | 0=待审核 1=被驳回 2=待出库 3=已完成 4=已撤销 5=作废 |
|
||||
| sendMode | number | 否 | 0=快递公司 1=自行派送 |
|
||||
| outSynchronizeState | number | 否 | 0=未同步 1=已同步 2=无需同步 |
|
||||
| assetSynchronizeState | number | 否 | -1=无需同步 0=未同步 1=已同步 |
|
||||
| useCustomerType / useCustomerId | number | 否 | 客户类型(0内部/1渠道商/2直营)和客户 ID |
|
||||
| useCustomerName | string | 否 | 客户名称,模糊匹配 |
|
||||
| outDateFrom / outDateTo | string | 否 | 出库日期范围,`yyyy-MM-dd`,结束日期含当天 |
|
||||
| applyDateFrom / applyDateTo | string | 否 | 申请日期范围,`yyyy-MM-dd`,结束日期含当天 |
|
||||
| page / rows | number | 否 | 页码默认 1;每页默认 20、上限 100 |
|
||||
|
||||
**出参 records 字段**:主单 `id/outOrderNo/allocationOrderNo/mallOrderNo`、`sourceType`
|
||||
(NORMAL/ALLOCATION/MALL)、代理和出库类型、`warehouseIds/warehouseNames`、出库/商品数量、状态码与文案、
|
||||
申请人/时间、客户、`schedulePeople`、`warehouseOutPeople/warehouseOutTimes`、发货物流、同步状态、
|
||||
关联送货/装机/取货单号、备注及创建/修改时间。
|
||||
|
||||
---
|
||||
|
||||
## 16. warehouse.out.detail.query
|
||||
|
||||
按主键或出库单号查询完整出库单详情;调拨出库单可直接查询,以 `sourceType=ALLOCATION` 标识。
|
||||
|
||||
- 分类:warehouse | 类型:读
|
||||
- 行级权限与 `warehouse.out.list.query` 完全一致,并加入同一个主表查询。
|
||||
- 不存在或无权访问统一返回 `NOT_FOUND_OR_FORBIDDEN`,不泄露记录是否存在。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | number | 条件必填 | 出库单主键;与 outOrderNo 至少传一个 |
|
||||
| outOrderNo | string | 条件必填 | 出库单号;与 id 至少传一个;同时传入时必须匹配同一条记录 |
|
||||
| includeSerials | boolean | 否 | 是否返回 SN 子行,默认 false |
|
||||
| serialLimit | number | 否 | SN 返回上限,默认 100、最大 500 |
|
||||
|
||||
**出参**:
|
||||
|
||||
- `record`:主单、来源类型、状态文案、客户、同步状态及关联工单号。
|
||||
- `warehouses`:物料汇总行涉及的仓库去重集合。
|
||||
- `items`:`parent_id=0` 物料汇总行,含物料、仓库、数量、价格和出库确认信息。
|
||||
- `serials/serialTotal/serialTruncated`:可选 SN 子行、完整总数和截断标记;未请求 SN 时
|
||||
`serials=[]`、`serialTruncated=false`。
|
||||
- `processes`:已处理记录(`schedule_state=2`),按操作时间、ID 正序,状态码与文案同时返回。
|
||||
- `address/logistics`:m1 收货地址及发货物流;无地址时 `address=null`。
|
||||
- `files.application/files.confirmation`:申请/确认图片,返回 `id/fileName/bucket/objectKey`;服务不持有
|
||||
OSS 签名密钥,不生成临时访问 URL。
|
||||
- `certificate`:出库凭证备注及凭证文件;均不存在时为 null。
|
||||
|
||||
详细设计与取舍见 `warehouse-out-mcp-tools-design.md`。
|
||||
|
||||
---
|
||||
|
||||
## 17. warehouse.in.list.query
|
||||
|
||||
分页查询入库单,参考旧系统 `POST /myWarehouseIn/allListByPage`,但将入库日期与申请日期分别过滤,
|
||||
并按本页主单 ID 批量加载仓库和待办信息。
|
||||
|
||||
- 分类:warehouse | 类型:读
|
||||
- 数据来源:m2 `warehouse_in`、`warehouse_in_details`、`warehouse_in_audit_process`
|
||||
- 行级权限:总部看全部;代理身份可看所属代理范围、所属或负责仓库涉及、或本人参与处理的入库单。
|
||||
- 单号支持入库/调拨/关联出库/商城/送货/装机/取货/撤机;另支持代理、仓库、入库类型、申请人、
|
||||
状态、同步状态、物流和入库/申请日期范围筛选。
|
||||
- 返回 `total/page/rows/records`;每条记录含 `sourceType`(ALLOCATION/OUTBOUND_LINKED/MALL/NORMAL)、
|
||||
状态文案、仓库集合、待办人、实际入库人和入库日期。
|
||||
|
||||
## 18. warehouse.in.detail.query
|
||||
|
||||
按 `id` 或 `inOrderNo` 查询详情,两者至少传一个;同时传入时必须匹配同一条可见记录。调拨入库单可直接查询。
|
||||
|
||||
- 行级权限与列表完全一致;不存在和无权统一返回 `NOT_FOUND_OR_FORBIDDEN`。
|
||||
- 返回 `record/sceneType/warehouses/items/processes/files/relatedOutOrder`。
|
||||
- `items` 按物料定义、物料索引和仓库汇总;`includeSerials` 默认 false,`serialLimit` 默认 100、上限 500,
|
||||
同时返回 `serialTotal/serialTruncated`。
|
||||
- `files.application/files.confirmation` 仅返回 `id/fileName/bucket/objectKey`。
|
||||
- `relatedOutOrder` 仅给出关联出库单号和查询工具名,不内嵌出库单内容。
|
||||
|
||||
详细设计见 `warehouse-in-mcp-tools-design.md`。
|
||||
|
||||
---
|
||||
|
||||
## 19. warehouse.todo.list.query
|
||||
|
||||
统一查询当前登录人的仓库待办,参考老系统 `/myWarehouseOut/needToBeDealtWith`、
|
||||
`/myWarehouseIn/needToBeDealtWith`、`/warehouseAllocation/warehouseAllocationProcessListByPage` 三个待办接口。
|
||||
|
||||
- 分类:warehouse | 类型:读
|
||||
- 数据来源:m2 流程视图 `warehouse_out_process` / `warehouse_in_process` / `warehouse_allocation_process`
|
||||
- 权限:待办是"人"的语义,只按流程表 `user_id = 当前登录人` 过滤,不叠加代理商行级权限。
|
||||
- 待办判定:`user_id = 当前登录人 AND soft_delete = 0 AND operation_type IN (0,1,2)`
|
||||
(3=纯日志排除)。`todoOnly` 默认 true 再限定 `schedule_state=1` 真待办。
|
||||
- 同一笔调拨最多以调拨审批/确认、出库执行、入库执行 3 个视角出现,不去重;
|
||||
派生自调拨的真实出入库单带 `derivedFrom="allocation"`,可按 `allocationOrderNo` 归并。
|
||||
- 分页实现:先全量查符合条件流程行按单据 id 内存去重 → 内存分页 → 按页 id 批量查视图行组装,防 N+1。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| docType | string | 否 | out=出库单 in=入库单 allocation=调拨/采购/划转单 all=全部,默认 all |
|
||||
| todoOnly | boolean | 否 | true=只返回真待办(默认);false=当前用户参与过的全部单据 |
|
||||
| orderState | number | 否 | 出库/入库单 0待审核 1被驳回 2待出库或待入库 3已完成 4已撤销 5作废;调拨类 0待审核 1被驳回 2待出库 3待入库 4已完成 5已撤销 6作废 |
|
||||
| orderNo | string | 否 | 单据主单号模糊匹配 |
|
||||
| page / rows | number | 否 | 页码默认 1;每页默认 20、上限 100,超出截断并在 `notice` 提示 |
|
||||
|
||||
**出参 records 字段**:`docType/docTypeText`(出库单/入库单/调拨单/采购单/划转单,
|
||||
按调拨视图 `op_type` 区分)、`docId/docNo`、`orderState/orderStateText`、
|
||||
`scheduleState/scheduleStateText`、`operationType/operationTypeText`(当前待办环节)、
|
||||
仓库名(allocation 为转出/转入仓库)、`agentId/agentName`、申请人/申请时间、
|
||||
`allocationOrderNo` 等关联单号、派生单 `derivedFrom="allocation"`;
|
||||
调拨类另含 `opType/opTypeText/allocationCount/nextUserName/inUserName/outUserName`。
|
||||
按申请时间倒序排列。
|
||||
|
||||
详细设计与老系统差异见 `warehouse-todo-mcp-tool-design.md`。
|
||||
|
||||
---
|
||||
|
||||
## 附录:在线查看工具清单
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# CRM 接入小趣 MCP 手册
|
||||
|
||||
> 适用版本:REQ-20260911-0013(增量同步框架)交付后
|
||||
> 端点:`https://agent.ququclean.com/mcp`(生产)/ `http://192.168.4.236:8088/mcp`(内网)
|
||||
> 协议:MCP Streamable HTTP(JSON-RPC),所有响应为 SSE,取 `data:` 行
|
||||
>
|
||||
> 当前生产可用性、阻塞项和下一步以[对接进度](crm-sync-integration-progress.md)为准;本手册描述目标调用方式,不代表发布门禁已经通过。
|
||||
|
||||
## 0. 边界(先读这段)
|
||||
|
||||
小趣 MCP 只做三件事:**供数据、供身份、守权限**。
|
||||
到期规则计算、AI 决策、推送、通知负责人、经营事项流转——全部是 CRM / 小趣 AI 系统的职责,MCP 不提供也不预留任何相关字段。
|
||||
|
||||
## 1. 一次性开通
|
||||
|
||||
| 步骤 | 操作方 | 说明 |
|
||||
|---|---|---|
|
||||
| 1 | 小趣管理员 | 登录 `https://agent.ququclean.com/`,进入「服务密钥」→「创建服务密钥」 |
|
||||
| 2 | 小趣管理员 | 调用方名称填 `crm`,勾选 `sync` 作用域,保存并立即复制只展示一次的 `X-Open-Key` |
|
||||
| 3 | CRM 后端 | 用下面的请求换取**长期服务 Token**(`xqak-...`),安全保管 |
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://agent.ququclean.com/api/open/token/ensure' \
|
||||
-H 'X-Open-Key: <管理后台创建后显示的服务密钥>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scopeCode":"sync","principalType":"service","serviceName":"crm"}'
|
||||
```
|
||||
|
||||
响应的 `token` 就是后续放进 MCP `Authorization: Bearer <token>` 的服务 Token。`X-Open-Key` 不是 MCP Token,不能直接放进 Authorization 头。
|
||||
|
||||
完成后 CRM 持有两样东西:**服务 Token**(后台扫描用)、**服务密钥**(重新 ensure 获取 Token 或给销售换个人 Token 用)。若还需要给销售换 QUERY 个人 Token,创建/编辑服务密钥时还要勾选对应 QUERY 作用域;只绑定 `sync` 的密钥不能跨域签发个人 Token。
|
||||
|
||||
密钥泄露时,管理员在同一页面点「轮换」(旧密钥立即失效)或「吊销」。数据库只保存密钥哈希,关闭创建/轮换结果弹窗后无法找回明文。
|
||||
|
||||
## 2. 每次调用前的身份自检
|
||||
|
||||
```
|
||||
tools/call integration.context.get
|
||||
```
|
||||
|
||||
返回 `principal_type` / `scopes` / `capability_levels` / `data_scope_version`。
|
||||
|
||||
- 后台扫描任务启动时必须确认 `principal_type=tenant_service` 且 capability 含 SYNC;拿错 Token(user_delegated)时**拒绝执行扫描**,防止串权。
|
||||
- `data_scope_version` 变化说明管理员改了授权范围,建议触发一次全量重建。
|
||||
|
||||
## 3. 后台通道:每日定时扫描(增量同步)
|
||||
|
||||
三个变化流工具:`users.changes.list` / `properties.changes.list` / `contracts.changes.list`。
|
||||
|
||||
入参只有三个(**禁止传 tenant_id/scope**,传了直接 PERMISSION_DENIED):
|
||||
|
||||
```json
|
||||
{ "syncToken": null, "cursor": null, "limit": 200 }
|
||||
```
|
||||
|
||||
### 协议语义
|
||||
|
||||
- **快照**:每轮同步签发 `snapshot_id` + `data_cutoff_at`,同一快照内翻页结果稳定,页间不会有数据漂移。
|
||||
- **游标**:页内按 `(modify_time, id)` 复合游标推进;`has_more=true` 时用 `next_cursor` 翻下一页。
|
||||
- **续传**:只有最后一页(`has_more=false`)才签发 `next_sync_token`。中断/宕机后从本地保存的 `sync_token + cursor` 继续,**不重不漏**。
|
||||
- **tombstone**:`deleted_at` 非空的条目是被删除的数据,CRM 本地镜像应标记删除,不进 AI。
|
||||
- **limit**:上限 200,传更大会被截断到 200 并带 notice。
|
||||
|
||||
### CRM 侧定时任务伪代码
|
||||
|
||||
```
|
||||
token, cursor = load_local() # 首次均为 null → 全量
|
||||
loop:
|
||||
resp = call("contracts.changes.list", {syncToken: token, cursor: cursor, limit: 200})
|
||||
for item in resp.items:
|
||||
if item.deleted_at: 本地镜像标记删除(item.contract_id)
|
||||
else: 本地镜像 upsert(item)
|
||||
save_local(token, resp.next_cursor) # 每页都存,断点恢复
|
||||
if resp.has_more: cursor = resp.next_cursor; continue
|
||||
save_local(resp.next_sync_token, null) # 本轮结束,钉死新游标
|
||||
break
|
||||
```
|
||||
|
||||
### 错误分支(必须实现)
|
||||
|
||||
| 错误码 | 含义 | CRM 动作 |
|
||||
|---|---|---|
|
||||
| `SYNC_CURSOR_EXPIRED` | 快照/游标过期或被篡改 | 清空本地镜像,syncToken=null 全量重建 |
|
||||
| `RATE_LIMITED` | 触发 SYNC 限流档 | 指数退避重试(建议 1s/2s/4s…上限 60s) |
|
||||
| `UNAUTHENTICATED` | Token 被吊销/人员停用 | 重新 ensure 取 Token,仍失败则告警人工介入 |
|
||||
|
||||
### 两个错误用法
|
||||
|
||||
- 不要拿 `version` 字段做并发控制——它由 `modify_time` 派生,只用于调试核对。
|
||||
- 不要把扫描任务和销售在线查询混用同一个 Token——服务 Token 调 QUERY 工具会被拒(scope 白名单),这是设计如此。
|
||||
|
||||
### 发送前复核(不能省略)
|
||||
|
||||
日同步只负责发现候选。CRM 在晨报、午报或日结真正入推送队列前,必须使用同一个服务 Token 批量调用:
|
||||
|
||||
```text
|
||||
tools/call contracts.current.batch_get
|
||||
arguments: {"contractIds":["1001","1002"]}
|
||||
```
|
||||
|
||||
- 每批 1—200 个稳定合同 ID,返回 `checked_at/count/items/not_found_ids`。
|
||||
- `items` 使用与合同变化流相同的公开字段;软删合同返回删除标记,查无记录进入 `not_found_ids`。
|
||||
- CRM 必须逐项复核状态、到期日、负责人、客户归属、提醒配置和续约结果。任一变化、缺失、删除、响应不完整、证据时间异常或服务不可用,对应合同本次都不得发送。
|
||||
- 该工具属于 SYNC 能力和限流档,调用方传 `tenant_id/tenantId/scope` 会被拒绝;数据域只能来自服务 Token。
|
||||
|
||||
## 4. 前台通道:销售在线查询(实时 ACL)
|
||||
|
||||
```
|
||||
1. 销售登录 CRM → CRM 后端调 /api/open/token/ensure {mobile: 销售手机号}
|
||||
幂等,同一人永远拿到同一个个人 Token;缓存 "CRM用户 ↔ xqak ↔ userId" 映射。
|
||||
2. 销售查合同时,带他的个人 Token 调:
|
||||
- contract.list.query (列表,12 字段,自动按实时 ACL 行过滤)
|
||||
- contract.detail.query (详情,含 version、files、processes)
|
||||
3. 个人 Token 调 SYNC 工具直接被 scope 白名单拦截——销售永远扫不到全量数据。
|
||||
```
|
||||
|
||||
个人 Token 缓存在 CRM 侧即可;收到 `UNAUTHENTICATED`(如人员被停用,60 秒内生效)时清缓存重取。
|
||||
|
||||
## 5. 两条通道速查
|
||||
|
||||
| | 前台(销售点查) | 后台(定时扫描) |
|
||||
|---|---|---|
|
||||
| Token 类型 | user_delegated | tenant_service |
|
||||
| 可用工具 | contract.list/detail.query 等 QUERY | *.changes.list + contracts.current.batch_get + context.get |
|
||||
| 数据范围 | 实时 ACL 行过滤 | 管理员授权的 sync scope |
|
||||
| 限流 | 常规档 | SYNC 独立档,互不占配额 |
|
||||
| tenant_id 入参 | 不允许 | 不允许,从 Token 推导 |
|
||||
|
||||
## 6. 通用输出规范
|
||||
|
||||
所有工具返回统一信封 `structuredContent`:
|
||||
|
||||
```json
|
||||
{ "schema_version": 2, "request_id": "...", "data": { ... } }
|
||||
```
|
||||
|
||||
- 请求头 `X-Mcp-Schema-Version: 2` 启用新信封;不带则返回老结构(v1,兼容期内有效)。
|
||||
- 所有请求把 `request_id` 记入 CRM 日志,排查时两边对得上。
|
||||
- **Token 和手机号明文不要落日志**(MCP 侧已对手机号打码,CRM 侧请同样处理)。
|
||||
|
||||
## 7. 后续扩展
|
||||
|
||||
同步协议是通用框架:以后新增设备/订单/工单同步,CRM 侧代码不变,只是多调一个 `xxx.changes.list`、多存一对 `sync_token/cursor`。输出信封与错误码也不变。
|
||||
@@ -0,0 +1,140 @@
|
||||
# SaaS MCP → 小趣 AI 经营系统对接进度
|
||||
|
||||
> MCP 系统独立进度入口。最后核对:2026-09-15;代码位于`codex/crm-contract-sync-review`本地评审分支。
|
||||
>
|
||||
> 本文只记录 `xiaoqu_mcp_service` 的责任和状态。小趣 AI / CRM 的规则、事项、AI、页面与推送进度,以其仓库 `PROJECT.md` 为准。
|
||||
|
||||
## 1. 两个系统的边界
|
||||
|
||||
| 系统 | 必须负责 | 明确不负责 |
|
||||
|---|---|---|
|
||||
| SaaS MCP | 服务身份、源租户/数据域、工具授权、源系统行级权限、用户/物业/合同标准变化流、稳定游标、审计与标准错误 | 到期规则、经营优先级、AI决策、经营事项状态机、销售页面、APNs推送 |
|
||||
| 小趣 AI / CRM | 租户连接、数据镜像、身份与客户映射、确定性规则、仅把命中数据交给AI、事项生成/流转、展示、操作、反馈、推送前复核 | 猜测源租户、绕过MCP权限、兼容不符合冻结Schema的错误字段 |
|
||||
|
||||
双方唯一连接面是版本化接口契约:`integration.context.get`、三条 `*.changes.list`、`contracts.current.batch_get`、统一错误信封,以及真实端到端验收。任何一侧内部实现都不能成为另一侧的隐式依赖。
|
||||
|
||||
## 2. 当前结论
|
||||
|
||||
当前状态:**协议可连接,公开字段Schema已在本地修正并通过回归;可靠同步与服务身份仍未通过,不能用于真实销售合同到期推送。**
|
||||
|
||||
交付状态:修正位于独立分支`codex/crm-contract-sync-review`,本地提交已形成;当前执行环境没有`wangheng/xiaoqu_mcp_service`写权限,尚未推送、合并或部署,生产服务仍是旧行为。
|
||||
|
||||
已经具备:
|
||||
|
||||
- MCP Streamable HTTP 会话、SSE响应和Schema v2信封。
|
||||
- `X-Open-Key` 管理、服务Token签发、Token作用域、工具白名单、审计和限流。
|
||||
- `integration.context.get`。
|
||||
- `users.changes.list`、`properties.changes.list`、`contracts.changes.list`及统一分页框架。
|
||||
- `contracts.current.batch_get`发送前批量回源复核已在本地实现;沿用SYNC身份、权限、限流和审计边界。
|
||||
- 基于 `(modify_time, id)` 的分页、软删标记、错误码和基础单元测试。
|
||||
|
||||
尚未通过的不是“小趣AI页面问题”,而是MCP公开数据契约与可靠同步语义。小趣AI侧不得通过放宽校验来掩盖这些问题。
|
||||
|
||||
## 3. 已确认问题与处理顺序
|
||||
|
||||
### P0-1 服务身份与源数据域语义混淆
|
||||
|
||||
代码核对结论:当前SaaS业务库没有统一的`tenant_id`字段。人员、物业、合同处于同一个业务数据域,在线行权限由代理商、物业和销售关系计算;合同负责人由`contract_new.sale_id`表达。不能为了对接新增一个虚假的行级租户字段。
|
||||
|
||||
现状仍有两个独立问题:`integration.context.get.tenant_id`由作用域编码拼接得到,`sync`被误当成数据域;服务Token又按`serviceName + scope`全局复用,创建它的Open Client身份没有进入Token。作用域只代表“能用什么工具”,不是数据来源或调用方身份。
|
||||
|
||||
影响:消费者无法稳定识别数据来自哪套SaaS实例;不同Open Client使用相同`serviceName`时可能复用服务Token。它不会替代合同负责人分发,但会影响连接隔离、吊销和审计。
|
||||
|
||||
标准改造:
|
||||
|
||||
1. 为每套MCP部署配置稳定的`source_domain_id`,例如生产SaaS数据域标识;它来自服务端配置,不由请求体、scope或名称推断。
|
||||
2. 服务Token持久化`open_client_id + subject_name`,ensure按调用方、服务主体和scope组合幂等签发;不同Open Client不共享Token。
|
||||
3. `integration.context.get`返回稳定`source_domain_id`。若为保持既有契约继续输出`tenant_id`,必须明确该字段在当前单数据域SaaS中表示“源数据域”,不能再返回`sync`。
|
||||
4. SYNC服务Token读取该单一数据域的完整变化流;记录按合同`owner_user_id`分发。在线个人查询继续使用现有人员ACL,两条通道不混用。
|
||||
5. 小趣AI的租户连接负责把自己的`tenant_id`显式绑定到该`source_domain_id`,禁止同一个源数据域被误配给多个目标租户。
|
||||
|
||||
验收:两个Open Client即使使用相同`serviceName=crm`也得到不同服务Token;吊销其中一个不影响另一个;上下文返回稳定源数据域;小趣AI连接错配源数据域时在同步前失败关闭。
|
||||
|
||||
### P0-2 三条变化流公开字段Schema(本地已修正,待部署联调)
|
||||
|
||||
原实现与冻结契约的差异:
|
||||
|
||||
| 流 | 当前输出 | 必须输出 |
|
||||
|---|---|---|
|
||||
| users | `mobile`;数值型`user_id/organization_ids` | `verified_mobile`;字符串型稳定ID;状态使用公开枚举 |
|
||||
| properties | 数值型`property_id` | 字符串型稳定ID;状态使用公开枚举 |
|
||||
| contracts | 数值型`contract_id/property_id/owner_user_id`、数值状态`0..4`、数值`expire_remind`,缺`remind_days` | 字符串型稳定ID、`PENDING/REJECTED/ACTIVE/EXPIRED/TERMINATED/VOID`、布尔提醒标记、实际提醒天数 |
|
||||
|
||||
已完成:新增独立公开DTO,将三类稳定ID统一为字符串;`mobile`改为`verified_mobile`;合同状态转换为公开枚举;`expire_remind`转换为布尔值;补齐`remind_days`;变化页使用统一`stream`字段。未知合同状态按`DATA_INTEGRITY_ERROR`失败关闭,不伪造业务语义。
|
||||
|
||||
验证:合并最新主分支后,Java编译、管理前端构建及MCP服务全量`229`项测试通过,`0`失败、`0`错误。尚需部署后的真实数据样例和小趣Gateway严格消费者契约联调,完成前仍不算生产通过。
|
||||
|
||||
验收:用真实样例对三条流执行消费者契约测试;Gateway无需兼容代码即可全部通过,未知字段、未知状态和错误类型必须失败。
|
||||
|
||||
### P0-3 当前“快照”不能保证重放稳定
|
||||
|
||||
现状:令牌只保存`cutoff`,每页仍直接查询持续变化的业务表。分页期间,`cutoff`之前的记录被修改、删除或权限变化时,相同`sync_token + cursor + limit`可能得到不同结果;秒级`modify_time`也可能漏掉同一行在边界秒内的再次修改。
|
||||
|
||||
影响:合同规模扩大或同步期间发生业务写入时,存在漏数、重复、记录消失和重放不一致风险,达不到文档声称的“不重不漏”。
|
||||
|
||||
标准改造:建立通用变更日志/Outbox(每次变更拥有单调递增`change_seq`,保存实体ID、操作类型、版本和必要投影),同步窗口按`change_seq`冻结;游标按`change_seq`推进。若使用CDC,也必须落成可保留、可重放的变化流,不能直接分页扫活表冒充快照。
|
||||
|
||||
验收:同步中并发更新、删除、负责人变更、权限变更以及相同请求重放时,旧快照结果不变,新变化只进入下一轮;压测覆盖成千上万合同。
|
||||
|
||||
### P0-4 交接Token错误复用了24小时快照TTL
|
||||
|
||||
现状:轮内快照Token、页游标和已完成轮的`next_sync_token`统一执行24小时TTL。
|
||||
|
||||
影响:每日任务只要稍晚、停机或积压,就可能被迫全量重建;数据量增大后会形成周期性全量压力。
|
||||
|
||||
标准改造:只给轮内快照/游标设置短TTL;交接Token按变更日志保留期校验,或保存持久化消费位点。过期时返回可观测的最早可用位置及标准错误,消费者执行受控重建。
|
||||
|
||||
验收:间隔超过24小时仍可正常增量;只有位点早于服务端保留期才触发重建。
|
||||
|
||||
### P0-5 数据权限版本不能覆盖真实权限变化
|
||||
|
||||
现状:`data_scope_version`仅取`token_scope`关系的修改时间;Open Client身份关联、scope-tool变化、源数据域ACL变化不会稳定反映。
|
||||
|
||||
影响:权限变化后旧同步位点可能继续使用,消费者无法判断需要重建或停止派发。
|
||||
|
||||
标准改造:维护数据域授权版本,任何调用方身份关联、工具授权、行级ACL语义变更都原子递增;上下文与同步Token使用同一个版本。
|
||||
|
||||
验收:修改任一影响可见数据的授权后,旧轮立即失败关闭,新轮在新数据域版本下重建。
|
||||
|
||||
### P0-6 发送前权威复核(本地已实现,待部署联调)
|
||||
|
||||
已实现`contracts.current.batch_get`:一次读取1—200份合同当前事实,按输入顺序返回已找到记录,并明确返回`not_found_ids`;软删记录不会伪装成“查无数据”。该工具沿用SYNC服务身份,禁止调用方覆盖数据域,使用SYNC独立限流档并进入统一审计。
|
||||
|
||||
小趣AI侧已经按同一契约实现短时发送凭证:只有状态、到期日、负责人、客户归属、提醒字段及续约结果与本地候选一致才允许R04进入推送;缺失、删除、字段变化、响应不完整或MCP不可用均失败关闭,R03不受影响。
|
||||
|
||||
尚未完成:本分支未推送和部署;生产`sync`作用域还必须绑定该新工具;需要用真实合同验证负责人变更、删除、到期日修改和服务不可用四类反向场景。
|
||||
|
||||
## 4. 依赖顺序
|
||||
|
||||
1. 冻结服务端`source_domain_id`配置、单数据域范围和合同状态/提醒字段字典。
|
||||
2. 完成Open Client与服务Token身份关联,补迁移、ensure和上下文测试。
|
||||
3. 建立通用变更日志/Outbox与保留策略,再迁移三条变化流;不继续强化活表分页方案。
|
||||
4. 使用独立公开DTO完成三条流字段映射和消费者契约测试。
|
||||
5. 修正交接Token生命周期与统一数据权限版本。
|
||||
6. 在测试环境用真实用户、物业、合同及变更/删除数据完成MCP侧验收。
|
||||
7. 交付Open Client、稳定源数据域ID和凭证给小趣AI侧,联合验证“同步→映射→规则→事项→展示→推送前复核”。
|
||||
|
||||
## 5. 状态看板
|
||||
|
||||
| 工作项 | 状态 | 责任系统 | 下一步 |
|
||||
|---|---|---|---|
|
||||
| MCP协议与工具发现 | 已完成 | MCP | 保持兼容并纳入回归 |
|
||||
| 服务密钥与Token基础生命周期 | 已完成 | MCP | 在租户模型完成后升级幂等键 |
|
||||
| 源数据域标识与服务主体隔离 | 未完成,P0阻塞 | MCP | 增加稳定部署数据域标识和Open Client关联 |
|
||||
| 三流公开字段Schema | 本地代码完成,待部署联调 | MCP | 部署后用真实三流样例跑Gateway严格契约测试 |
|
||||
| 发送前合同当前状态批量复核 | 本地代码完成,待部署联调 | MCP + 小趣AI | 绑定sync作用域并完成四类真实反向用例 |
|
||||
| 可靠变化日志与稳定快照 | 未完成,P0阻塞 | MCP | 设计Outbox/CDC及保留策略 |
|
||||
| 权限版本与交接位点生命周期 | 未完成,P0阻塞 | MCP | 与租户/变化日志模型一起实现 |
|
||||
| 小趣AI连接与消费代码 | 已完成,待部署联调 | 小趣AI | 见小趣AI仓库`PROJECT.md` |
|
||||
| 真实销售合同到期推送 | 未开放 | 双方联合验收 | 所有P0通过后才允许从Shadow转Live |
|
||||
|
||||
## 6. 发布门禁
|
||||
|
||||
以下条件全部满足前,不得把MCP同步标记为生产可用,也不得向真实销售发送合同提醒:
|
||||
|
||||
- 稳定源数据域、服务主体隔离和连接错配反向用例通过。
|
||||
- 三条变化流通过冻结Schema及真实数据样例校验。
|
||||
- 并发写入、删除、断点续传、超过24小时续传和权限变化测试通过。
|
||||
- 凭证不入库明文、不入日志;轮换和吊销测试通过。
|
||||
- 小趣AI侧完成负责人、客户双映射和发送前实时复核。
|
||||
- 完整链路具有`request_id/snapshot_id/source_domain_id`审计证据,可定位每条事项和推送的数据来源。
|
||||
@@ -0,0 +1,59 @@
|
||||
# 入库单 MCP 工具设计
|
||||
|
||||
## 1. 范围与评审结论
|
||||
|
||||
参考旧系统 `POST /myWarehouseIn/allListByPage` 与 `GET /myWarehouseIn/lookDetail`,新增:
|
||||
|
||||
| 工具 | 分类 | 用途 |
|
||||
|---|---|---|
|
||||
| `warehouse.in.list.query` | warehouse/read | 分页查询入库单 |
|
||||
| `warehouse.in.detail.query` | warehouse/read | 按主键或入库单号查询详情 |
|
||||
|
||||
自评审通过,确认采用以下产品取舍:调拨入库单允许直接查询详情;关联出库单仅返回索引,
|
||||
不内嵌完整出库信息。工具只读 m2,不新增业务表或 Flyway migration;启动后由
|
||||
`ToolSyncService` 自动登记,需由管理员绑定 Scope 后才可调用。
|
||||
|
||||
## 2. 列表
|
||||
|
||||
- 单号:入库、调拨、关联出库、商城、送货、装机、取货、撤机单号。
|
||||
- 归属:代理、仓库、入库类型、申请人。
|
||||
- 状态与物流:订单状态、入库同步状态、物流公司和运单号。
|
||||
- 日期:`inDateFrom/inDateTo` 精确过滤 `in_date`;`applyDateFrom/applyDateTo`
|
||||
过滤 `apply_date`,结束日期含当天。不会复制旧接口将入库日期改写成申请日期的异常逻辑。
|
||||
- 分页:page 默认 1,rows 默认 20、上限 100。
|
||||
|
||||
返回主单、来源类型、状态文案、涉及仓库、待办人、实际入库人及日期、物流和关联单号。
|
||||
本页明细与待办按主单 ID 批量查询,避免 N+1。
|
||||
|
||||
`sourceType` 的优先级为 `ALLOCATION`、`OUTBOUND_LINKED`、`MALL`、`NORMAL`。
|
||||
|
||||
## 3. 详情
|
||||
|
||||
`id`、`inOrderNo` 至少传一个;同时传入时必须匹配同一条可见记录。详情返回:
|
||||
|
||||
- `record`、`sceneType`、涉及仓库;
|
||||
- 按物料定义、物料索引和仓库汇总的 `items`;
|
||||
- 可选 `serials` 以及完整 `serialTotal`、`serialTruncated`;
|
||||
- `schedule_state=2` 的已处理流程;
|
||||
- `file_type=0/1` 的申请、确认入库附件,仅返回 OSS 对象定位,不生成签名 URL;
|
||||
- `relatedOutOrder`:仅出库单号和建议调用的 `warehouse.out.detail.query` 工具名。
|
||||
|
||||
## 4. 权限与安全
|
||||
|
||||
列表和详情强制复用 `PermissionService.applyWarehouseInRowFilter`:
|
||||
|
||||
- 总部身份查看全部;
|
||||
- 代理身份可见范围是主单所属代理范围、所属或负责仓库涉及、本人参与处理三类并集;
|
||||
- 代理范围包含自己、直属下级和 `warehouse_agent_auth` 授权代管代理;
|
||||
- 权限组异常或代理用户未关联代理商时失败关闭;
|
||||
- 详情不存在与无权访问统一返回 `NOT_FOUND_OR_FORBIDDEN`。
|
||||
|
||||
权限使用主查询中的 `EXISTS`,所有业务筛选均只能缩小结果。关联出库不提前判断是否可访问,
|
||||
避免形成旁路探测;调用方若需要内容,必须再调用出库详情工具并接受其独立鉴权。
|
||||
|
||||
## 5. 验收
|
||||
|
||||
- 列表分页、日期校验、仓库过滤、批量富化和状态文案;
|
||||
- 详情双条件、调拨单、物料汇总、SN 默认关闭/截断、附件分类、出库索引;
|
||||
- 总部、代理范围、仓库负责人、流程参与人和异常配置失败关闭;
|
||||
- 固定 `TZ=Asia/Shanghai` 执行全量后端测试,不连接生产数据库、不执行部署。
|
||||
@@ -0,0 +1,91 @@
|
||||
# 出库单 MCP 工具设计
|
||||
|
||||
## 1. 背景与范围
|
||||
|
||||
参考旧系统 `POST /myWarehouseOut/allListByPage` 和 `GET /myWarehouseOut/lookDetail`,在
|
||||
`xiaoqu-agent-service` 新增两个只读工具:
|
||||
|
||||
| 工具 | 分类 | 用途 |
|
||||
|---|---|---|
|
||||
| `warehouse.out.list.query` | warehouse/read | 分页查询出库单 |
|
||||
| `warehouse.out.detail.query` | warehouse/read | 按主键或出库单号查询完整详情 |
|
||||
|
||||
只读取 m0/m1/m2,主要数据表为 m2 的 `warehouse_out`、`warehouse_out_details`、
|
||||
`warehouse`、`warehouse_out_audit_process`、`warehouse_in_out_pic`、
|
||||
`warehouse_out_certificate`;收货地址和物流信息按需读取 m1。
|
||||
|
||||
本次不返回关联入库单详情,不修改旧模块和业务表,不新增 Flyway migration。新工具由现有
|
||||
`ToolSyncService` 自动登记,初始不属于任何 Scope,发布后需在管理后台授权。
|
||||
|
||||
## 2. 列表工具
|
||||
|
||||
### 2.1 入参
|
||||
|
||||
- 单号模糊查询:`outOrderNo`、`allocationOrderNo`、`mallOrderNo`、`deliveryOrderNo`、
|
||||
`installOrderNo`、`pickupOrderNo`。
|
||||
- 归属精确查询:`agentId`、`warehouseId`、`warehouseTypeId`、`applyUserId`。
|
||||
- 状态精确查询:`orderState`、`sendMode`、`outSynchronizeState`、`assetSynchronizeState`。
|
||||
- 客户查询:`useCustomerType`、`useCustomerId` 精确查询,`useCustomerName` 模糊查询。
|
||||
- 日期:`outDateFrom/outDateTo` 过滤 `out_date`;`applyDateFrom/applyDateTo` 过滤
|
||||
`apply_date`。日期格式固定为 `yyyy-MM-dd`,业务时区为 `Asia/Shanghai`,结束日期包含当天。
|
||||
- 分页:`page` 默认 1;`rows` 默认 20、上限 100,超出上限截断并返回 `notice`。
|
||||
|
||||
旧接口错误地把出库日期条件改写为申请日期条件,本工具不复制该行为。
|
||||
|
||||
### 2.2 出参
|
||||
|
||||
顶层统一返回 `total/page/rows/records`。每条记录包含主单号、来源类型、代理商、出库类型、
|
||||
仓库集合、数量、状态及状态文案、申请人及申请时间、客户、待办人、出库确认人及日期、物流、
|
||||
同步状态、关联工单号和备注。
|
||||
|
||||
`sourceType`:有调拨单号为 `ALLOCATION`,否则有商城订单号为 `MALL`,其余为 `NORMAL`。
|
||||
|
||||
`orderState`:0=待审核、1=被驳回、2=待出库、3=已完成、4=已撤销、5=作废。
|
||||
|
||||
列表页的仓库、待办人、出库人均按本页主单 ID 批量查询和内存聚合,禁止逐单 N+1 查询。
|
||||
|
||||
## 3. 详情工具
|
||||
|
||||
### 3.1 入参
|
||||
|
||||
- `id`、`outOrderNo` 至少传一个;同时传入时必须指向同一条可见记录。
|
||||
- `includeSerials` 默认 `false`。
|
||||
- `serialLimit` 默认 100、最大 500;只限制返回数量,`serialTotal` 始终返回完整数量。
|
||||
|
||||
### 3.2 出参
|
||||
|
||||
- `record`:主单字段、来源、状态文案、客户、同步状态和关联工单号。
|
||||
- `warehouses`:出库明细涉及的仓库 ID/名称去重集合。
|
||||
- `items`:`parent_id=0` 的物料汇总行,含物料编号、名称、分类、规格、仓库、数量、单价、
|
||||
总额、出库人及时间。
|
||||
- `serials`:仅 `includeSerials=true` 返回 SN 子行;另返回 `serialTotal/serialTruncated`。
|
||||
- `processes`:`schedule_state=2` 的已处理记录,按操作时间、ID 正序,状态码与文案同时返回。
|
||||
- `address`:商城订单地址或主单地址。
|
||||
- `logistics`:发货方式、快递公司、运单号、配送人和收货信息。
|
||||
- `files.application/files.confirmation`:申请图片和确认出库图片。服务不持有 OSS 签名密钥,
|
||||
返回文件 ID、文件名及存储对象定位信息,不生成临时访问 URL。
|
||||
- `certificate`:凭证备注与 `file_type=4/in_out_allocation_type=99` 的凭证文件。
|
||||
|
||||
调拨出库单允许直接查询,并以 `sourceType=ALLOCATION` 标识。
|
||||
|
||||
## 4. 行级权限
|
||||
|
||||
列表和详情使用完全相同的主查询权限条件,业务筛选只能缩小结果:
|
||||
|
||||
- 总部身份(权限组 `isagent=0`):不过滤。
|
||||
- 代理商身份(权限组 `isagent=1`):可见范围为以下条件的并集:
|
||||
- 主单 `agent_id` 属于自己、直属下级或 `warehouse_agent_auth` 授权代管代理;
|
||||
- 出库明细涉及的仓库,其 `warehouse.agent_id` 或 `warehouse.manager_agent_id` 属于上述代理范围;
|
||||
- 当前用户存在该主单的未删除审核/处理记录。
|
||||
- 代理商用户未关联代理商、权限组不存在或配置异常时失败关闭。
|
||||
|
||||
详情不存在与无权访问统一抛出 `NOT_FOUND_OR_FORBIDDEN`,防止通过 ID/单号探测数据。
|
||||
|
||||
权限通过主表查询中的关联 `EXISTS` 条件完成,不先查询大量 ID 再拼接 `IN`。
|
||||
|
||||
## 5. 测试与验收
|
||||
|
||||
- 列表:分页上限、各类日期/状态/仓库条件、批量聚合和状态文案。
|
||||
- 详情:ID/单号参数校验、双条件一致性、调拨单、SN 默认关闭与截断、附件/凭证分组。
|
||||
- 权限:总部、代理范围、负责管理仓库、处理人、无代理失败关闭;详情查询必须复用相同过滤器。
|
||||
- 构建:固定 `TZ=Asia/Shanghai` 执行后端测试;不连接生产数据库、不执行部署。
|
||||
@@ -0,0 +1,103 @@
|
||||
# 仓库待办 MCP 工具设计(REQ-20260915-0008)
|
||||
|
||||
## 1. 范围与评审结论
|
||||
|
||||
老系统有 4 个"待办"接口,本需求统一为一个工具:
|
||||
|
||||
| 老系统接口 | 数据来源 |
|
||||
|---|---|
|
||||
| `POST /myWarehouseOut/needToBeDealtWith` | m2 视图 `warehouse_out_process` |
|
||||
| `POST /myWarehouseIn/needToBeDealtWith` | m2 视图 `warehouse_in_process` |
|
||||
| `POST /warehouseAllocation/warehouseAllocationProcessListByPage`(调拨/划转/采购共用) | m2 视图 `warehouse_allocation_process` |
|
||||
|
||||
新增工具:`warehouse.todo.list.query`(warehouse/read),统一查询当前登录人的仓库待办。
|
||||
|
||||
## 2. 待办语义(以老系统为准)
|
||||
|
||||
流程视图一行 = 单据 × 处理人 × 待办环节。待办判定:
|
||||
|
||||
```
|
||||
user_id = 当前登录人 AND soft_delete = 0 AND operation_type IN (0, 1, 2)
|
||||
```
|
||||
|
||||
- `operation_type=3` 为"驳回或修改记录",纯日志不需要待办,排除(出入库视图);
|
||||
调拨视图 `operation_type=4`(撤销)同为纯日志,也被 IN (0,1,2) 排除。
|
||||
- 不按代理商过滤:待办是"人"的语义,不是代理商数据权限语义。
|
||||
老系统出入库待办同样只按 `user_id` 过滤(`input.setUserId(user.getId())`),
|
||||
只有调拨待办显式 `eq("user_id", user.getId())` 且代理行级过滤被注释掉,
|
||||
与本实现一致。
|
||||
|
||||
`operation_type` 枚举(老系统视图字段注释,xiaoqu-publicbeanandmap beans/b2):
|
||||
|
||||
| 值 | 出库/入库视图 | 调拨视图 |
|
||||
|---|---|---|
|
||||
| 0 | 审批流操作 | 审批流操作 |
|
||||
| 1 | 确认出库 / 确认入库 | 确认入库操作 |
|
||||
| 2 | 本人修改(需申请人待办) | 确认出库操作 |
|
||||
| 3 | 驳回或修改操作(纯日志,排除) | 本人修改 |
|
||||
| 4 | — | 撤销(纯日志,排除) |
|
||||
|
||||
`schedule_state`:0=无需待办,1=待办,2=已办。
|
||||
|
||||
`order_state`:出库/入库单 0=待审核 1=被驳回 2=待出库/待入库 3=已完成 4=已撤销 5=作废;
|
||||
调拨类 0=待审核 1=被驳回 2=待出库 3=待入库 4=已完成 5=已撤销 6=作废。调拨视图 `op_type`:
|
||||
0=调拨(调拨单),1=采购(采购单),2=划转(划转单)。
|
||||
|
||||
## 3. 单据类型与调拨三视角
|
||||
|
||||
- 出库单(24 开头单号)、入库单(23 开头)是独立表。
|
||||
- 调拨/采购/划转是同一张 `warehouse_allocation` 表,`op_type` 区分。
|
||||
- 调拨/划转审批通过后由 `WarehouseServiceImpl.procAfterAllocation` 生成真实入库单 +
|
||||
出库单并回填 `allocation_order_no`,同一笔调拨最多以 3 个视角出现在待办里
|
||||
(调拨审批/确认、出库执行、入库执行)。这是正常业务,**不去重**;
|
||||
派生出入库单带 `derivedFrom="allocation"` 标记,客户端可按 `allocationOrderNo` 归并。
|
||||
|
||||
## 4. 工具契约
|
||||
|
||||
入参全部可选:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| docType | out / in / allocation / all,默认 all;allocation 涵盖调拨/采购/划转 |
|
||||
| todoOnly | 默认 true 只返回 `schedule_state=1` 真待办;false 返回当前用户参与过的全部单据 |
|
||||
| orderState | 按单据状态过滤(枚举值见上,写入 description) |
|
||||
| orderNo | 单据主单号模糊匹配(出库/入库/调拨单号) |
|
||||
| page / rows | 默认 1 / 20,rows >100 截断为 100 并在 notice 提示 |
|
||||
|
||||
出参:`total/page/rows/records`。每条 record 统一结构:`docType/docTypeText`
|
||||
(出库单/入库单/调拨单/采购单/划转单)、`docId/docNo`、`orderState/orderStateText`、
|
||||
`scheduleState/scheduleStateText`、`operationType/operationTypeText`(当前待办环节)、
|
||||
仓库名(allocation 为 `warehouseOutName/warehouseInName`)、`agentId/agentName`、
|
||||
申请人/申请时间、`allocationOrderNo` 等关联单号(out/in 另含 `mallOrderNo`,
|
||||
in 另含关联 `outOrderNo`)、派生单 `derivedFrom="allocation"`;
|
||||
allocation 记录另含 `opType/opTypeText/allocationCount/allocationDate/nextUserName/inUserName/outUserName`。
|
||||
|
||||
排序:申请时间倒序(null 在后),同时间按单据 id 倒序。
|
||||
|
||||
## 5. 实现要点
|
||||
|
||||
- **两阶段分页**:阶段一按过滤条件全量查流程行(只 select 单据 id + 申请时间),
|
||||
内存按单据 id 去重;内存分页;阶段二按页内 id 批量查视图整行组装。
|
||||
避开老系统 `GROUP BY` 下 `selectCount` 不可靠的坑,天然防 N+1。
|
||||
(注意:阶段一只取 id 列时若同一单据多行需内存去重,本实现用 `putIfAbsent`。)
|
||||
- **多环节取行**:一个单据对同一用户可能有多行(已办 + 待办),`todoOnly=false` 时
|
||||
优先取 `schedule_state=1` 的行作为"当前待办环节";`todoOnly=true` 时全部行均为待办,取首行。
|
||||
- 权限:只用 `AuthContextHolder` 当前用户 id 做 `user_id` 过滤,
|
||||
不叠加 `PermissionService` 行级过滤。
|
||||
|
||||
## 6. 与老系统的已知差异
|
||||
|
||||
- 老系统出入库待办接口按 `GROUP BY (单据id, schedule_state, operation_type)` 分页,
|
||||
同一单据可能按环节拆成多行返回;本实现按单据聚合为一条记录,
|
||||
`operationType/scheduleState` 取当前待办环节。更符合"待办列表"语义。
|
||||
- 老系统调拨待办不过滤 `operation_type`(会带入撤销日志行);本实现按统一语义
|
||||
IN (0,1,2) 排除,同时把调拨视图被驳回后"本人修改"(type=3)一并排除
|
||||
(与出入库视图 type=3 语义对齐的取舍)。如需申请人修改待办可再评估。
|
||||
- 老系统出入库待办前端可传 `schedule_state` 筛选"已办";本实现用 `todoOnly=false` 表达。
|
||||
|
||||
## 7. 验收
|
||||
|
||||
- 三类型合并排序分页、rows 截断 notice、派生单 derivedFrom 标记;
|
||||
- todoOnly=true/false 两态、orderState/orderNo/docType 过滤、非法 docType 拒绝;
|
||||
- Spring AI schema 发布(@Tool + @XqTool 自动注册,ToolSyncService 同步 tool 表);
|
||||
- 固定 `TZ=Asia/Shanghai` 执行全量后端测试,不连接生产数据库、不执行部署。
|
||||
@@ -0,0 +1,153 @@
|
||||
# xiaoqu-ai 接入 xiaoqu-mcp 详细设计
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
本文是 [`xiaoqu 多产品 Agent 统一接入 xiaoqu-mcp 总体设计`](./xiaoqu-mcp-multi-product-agent-design.md) 的 xiaoqu-ai 落地设计。
|
||||
|
||||
xiaoqu-ai 的 iOS、Android、Web 共用一套服务端 MCP 接入,不在三个客户端分别实现 MCP Client:
|
||||
|
||||
```text
|
||||
iOS / Android / Web
|
||||
│ 产品登录 Token
|
||||
▼
|
||||
xiaoqu-ai Gateway / Agent Runtime
|
||||
│ 用户 MCP Token(仅服务端持有)
|
||||
▼
|
||||
xiaoqu-agent-service /mcp
|
||||
```
|
||||
|
||||
项目目录:
|
||||
|
||||
- iOS、Web、Gateway、Agent Service:`/Users/tonylai/xiaoquai/xiaoqu-ai`
|
||||
- Android:`/Users/tonylai/jiaxk/xiaoqu-ai-android`
|
||||
|
||||
## 2. 当前代码依据
|
||||
|
||||
| 端/服务 | 当前入口 | 结论 |
|
||||
|---|---|---|
|
||||
| iOS 销售助手 | `ios/Sources/XiaoquCRM/Features/SalesAgent/SalesAgentClient.swift` | 继续调用 `/api/v1/sales-agent/*` REST/SSE |
|
||||
| iOS 管理助手 | `ios/Sources/XiaoquCRM/Features/AI/Networking/ManagerAgentClient.swift` | 继续调用服务端 Agent,不新增 MCP Token |
|
||||
| Android | `app/src/main/java/com/xiaoquai/crm/data/remote/WebSocketManager.kt`、`ApiService.kt` | 继续走 Gateway WebSocket/REST,`TokenManager` 只保存产品 Token |
|
||||
| Web | `web/src/hooks/use-agent-chat.ts` | 继续调用 `/api/v1/agents/{id}/invoke` |
|
||||
| Gateway | `gateway/internal/agent/tools.go`、`gateway/internal/handler/runtime_tool_invoke_handlers.go` | 已有统一 ToolExecutor、数据范围校验和运行时工具入口 |
|
||||
| Agent Service | `agent-service/agent_service/tools/runtime_gateway_tools.py` | 已使用当前用户 JWT调用 Gateway runtime catalog/tool invoke |
|
||||
|
||||
iOS 的 `ClawBridge.swift` 是独立端侧 Agent 模式,不属于本次服务端 xiaoqu-mcp 接入,不修改其 Keychain、MCP 配置或 Token。
|
||||
|
||||
## 3. 统一接入点
|
||||
|
||||
MCP 适配器放在 Gateway 的工具执行层,三端最终复用同一实现:
|
||||
|
||||
```text
|
||||
Web / Android Agent 请求 ──► Gateway AgentRuntime ─┐
|
||||
├─► Gateway ToolExecutor
|
||||
iOS Sales Agent ──► agent-service │ │
|
||||
└─ runtime_gateway_tools ───────┘ ▼
|
||||
xiaoqu-mcp
|
||||
```
|
||||
|
||||
这样保留现有客户端和 Agent 编排,只把新的远程工具接入集中在 Gateway。Agent Service 继续转发已验证的用户 JWT,不自行持有第二份 xiaoqu-mcp Token 缓存。
|
||||
|
||||
## 4. Gateway 变更
|
||||
|
||||
### 4.1 配置
|
||||
|
||||
在 Gateway 启动配置中增加:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `XQ_MCP_ENABLED` | `false` | 是否启用 xiaoqu-mcp 工具 |
|
||||
| `XQ_MCP_URL` | unset | xiaoqu-mcp 内网地址 |
|
||||
| `XQ_MCP_OPEN_KEY` | unset | Token 初始化凭证 |
|
||||
| `XQ_MCP_SCOPE_CODE` | unset | xiaoqu-ai 固定 scope;启用前必须在管理后台确认 |
|
||||
| `XQ_MCP_TIMEOUT_SECONDS` | `10` | MCP 请求超时 |
|
||||
|
||||
启用时 URL、Open Key 和 scope 必填。Open Key 只来自部署 Secret,不写入 YAML、客户端配置或仓库。
|
||||
|
||||
### 4.2 最小模块
|
||||
|
||||
新增 `gateway/internal/connector/xiaoqumcp/`:
|
||||
|
||||
- `token_provider.go`
|
||||
- 输入 Gateway 已验证的 `tenantID`、`userID`。
|
||||
- 从用户资料存储取得可信 mobile/email 并调用 `ensure`。
|
||||
- 按租户、用户和 scope 缓存 Token;401 时只刷新当前用户一次。
|
||||
- `client.go`
|
||||
- 管理 MCP JSON-RPC、Session ID、Bearer Header、超时和错误映射。
|
||||
- 按租户、用户和 scope 隔离 Session。
|
||||
|
||||
使用 Go 标准 HTTP Client 和项目现有错误/日志设施,不增加 MCP 框架依赖。
|
||||
|
||||
### 4.3 ToolExecutor 接入
|
||||
|
||||
在 `gateway/internal/agent/tools.go` 注册稳定的本地工具名,由 Handler 调用 `xiaoqumcp.Client`。不得把远端 `tools/list` 原样注册给模型。
|
||||
|
||||
首期只接只读工具。候选映射以 xiaoqu-mcp 当前工具为基础,但必须先为 xiaoqu-ai 创建并审核独立 scope:
|
||||
|
||||
| Gateway 本地工具 | MCP 工具 | 状态 |
|
||||
|---|---|---|
|
||||
| `xiaoqu.agent_count` | `agent.count` | scope 绑定后启用 |
|
||||
| `xiaoqu.agent_list` | `agent.list.query` | scope 绑定后启用 |
|
||||
| `xiaoqu.property_list` | `property.list.query` | scope 绑定后启用 |
|
||||
|
||||
每个工具继续使用现有 `RequiredDataScope`、Agent Capability 绑定和参数 schema。MCP 返回结果先做字段白名单投影,再进入模型上下文、卡片渲染和 `chat_tool_logs`。
|
||||
|
||||
## 5. 用户与权限流程
|
||||
|
||||
```text
|
||||
产品登录 Token
|
||||
→ Gateway middleware 得到 userID / tenantID / role
|
||||
→ Agent Capability + RequiredDataScope 校验
|
||||
→ 可信用户资料解析 mobile/email
|
||||
→ token_provider.ensure(scopeCode)
|
||||
→ mcp_client.tools/call
|
||||
→ xiaoqu-mcp 再校验 Token scope 与数据权限
|
||||
```
|
||||
|
||||
`agent.InvokeRequest` 已携带 `UserID`、`TenantID`、`UserJWT` 和 `UserRole`。MCP Handler 必须使用这些可信字段,不接受模型参数覆盖身份或 scope。
|
||||
|
||||
Agent Service 的 `runtime_gateway_tools.py` 继续转发入站用户 JWT;无 JWT 时保持 fail-closed,不允许自签或使用公共用户身份。
|
||||
|
||||
## 6. 客户端影响
|
||||
|
||||
| 客户端 | 变更 |
|
||||
|---|---|
|
||||
| iOS | 无 MCP 网络层变更;继续消费现有 REST/SSE、确认卡和工具结果 |
|
||||
| Android | 无 MCP 网络层变更;Gateway Token 仍由现有 `TokenManager` 管理 |
|
||||
| Web | 无 MCP 网络层变更;继续调用 Agent invoke API 并渲染返回结果 |
|
||||
|
||||
如果新工具需要专用展示,只扩展现有服务端响应投影和客户端卡片 schema;不得因此向客户端下发 MCP Token、Session ID 或原始 Header。
|
||||
|
||||
## 7. 错误与生命周期
|
||||
|
||||
- 用户切换或产品会话失效时,当前用户的 MCP Token 缓存和 Session 一并失效。
|
||||
- MCP 401:清理当前用户缓存,重新 `ensure` 并重连一次。
|
||||
- MCP 403:返回权限不足,不刷新 Token。
|
||||
- MCP 超时或不可用:返回现有稳定工具错误,不回退公共 Token 或数据库直连。
|
||||
- 只读请求最多重试一次;写工具不在首期范围。
|
||||
- 日志和 `chat_tool_logs` 只记录工具名、状态、耗时与脱敏错误,不记录 Token、Header、mobile/email 或未投影原始结果。
|
||||
|
||||
## 8. 开发顺序
|
||||
|
||||
1. 在目标环境创建或确认 xiaoqu-ai 独立 scope,并绑定首期只读工具。
|
||||
2. 增加 Gateway 配置与启动校验。
|
||||
3. 实现 `token_provider.go` 和 `client.go`,覆盖 Token、Session、超时和错误映射测试。
|
||||
4. 在 ToolExecutor 注册固定工具并复用现有 DataScope 校验。
|
||||
5. 验证 Agent Service runtime gateway 路径仍只转发用户 JWT。
|
||||
6. 分别用 iOS、Android、Web 完成同用户、跨用户和跨租户联调。
|
||||
|
||||
## 9. 项目验收
|
||||
|
||||
- 同一用户在 iOS、Android、Web 调用同一工具时,结果契约和数据权限一致。
|
||||
- 三端请求、存储、日志中均无 MCP Token、Session ID 或 `X-Open-Key`。
|
||||
- Web/Android Gateway Agent 与 iOS Sales Agent 最终调用同一个 Gateway MCP Handler。
|
||||
- 用户 A/B、不同租户和 scope 的 Token 缓存与 MCP Session 完全隔离。
|
||||
- 无本地 Capability、无 RequiredDataScope 或无 MCP scope 时均不能调用。
|
||||
- MCP 401 只刷新当前用户;MCP 不可用不影响未启用该 Capability 的原有 Agent 功能。
|
||||
|
||||
## 10. 当前不做
|
||||
|
||||
- 不为 iOS、Android、Web 分别实现 MCP Client。
|
||||
- 不改造 iOS ClawBridge。
|
||||
- 不把远端全部工具动态暴露给 Agent。
|
||||
- 不在首期接入写工具或副作用自动重试。
|
||||
@@ -1,232 +0,0 @@
|
||||
# xiaoqu 多端 Agent 统一接入 xiaoqu-mcp 开发设计
|
||||
|
||||
## 1. 设计结论
|
||||
|
||||
xiaoqu-ai 的 iOS、Android、Web **共用一套 Agent/MCP 接入链路**:
|
||||
|
||||
```text
|
||||
iOS / Android / Web
|
||||
│ 产品登录 Token
|
||||
▼
|
||||
xiaoqu-ai Gateway / Agent Runtime
|
||||
│ 用户 MCP Token(仅服务端持有)
|
||||
▼
|
||||
xiaoqu-agent-service /mcp
|
||||
```
|
||||
|
||||
运营项目是另一套产品和 Agent 编排服务,因此保留自己的入口,但复用同一个
|
||||
xiaoqu-mcp 服务、Token 原语、工具契约、权限和审计规则:
|
||||
|
||||
```text
|
||||
运营 iOS / Android
|
||||
│ 产品登录 Token
|
||||
▼
|
||||
ops-assistant-service
|
||||
│ 用户 MCP Token(仅服务端持有)
|
||||
▼
|
||||
xiaoqu-agent-service /mcp
|
||||
```
|
||||
|
||||
共同的是服务能力和协议,不是 Swift/Kotlin UI 代码。客户端不实现 MCP Client,
|
||||
不保存 MCP Token,也不接触 `X-Open-Key`。
|
||||
|
||||
### 1.1 范围
|
||||
|
||||
| 产品 | 客户端 | 项目目录 |
|
||||
|---|---|---|
|
||||
| xiaoqu-ai | iOS、Web、服务端 Agent | `/Users/tonylai/xiaoquai/xiaoqu-ai` |
|
||||
| xiaoqu-ai | Android | `/Users/tonylai/jiaxk/xiaoqu-ai-android` |
|
||||
| xiaoqu-operations-ai | iOS、Android、服务端 Agent | `/Users/tonylai/jiaxk/xiaoqu-operations-ai` |
|
||||
| xiaoqu-mcp | MCP 服务、Token、权限、审计 | 当前仓库 `xiaoqu-agent-service` |
|
||||
|
||||
### 1.2 明确不做
|
||||
|
||||
- 不为 iOS、Android、Web 分别实现 MCP Client。
|
||||
- 不提供面向 App/Web 的 MCP Token 换取接口。
|
||||
- 不把固定 MCP Token、`X-Open-Key` 写入 App、浏览器配置、prompt 或 Git。
|
||||
- 不让客户端、Planner 或模型提交 `user_id`、手机号、邮箱、租户或 scope 作为授权依据。
|
||||
- 不改造现有 iOS ClawBridge;它是独立的端侧 Agent 模式,不属于本次 xiaoqu-mcp 接入链路。
|
||||
|
||||
## 2. 当前代码依据
|
||||
|
||||
| 项目/端 | 当前入口 | 结论 |
|
||||
|---|---|---|
|
||||
| xiaoqu-ai iOS | `ios/Sources/XiaoquCRM/Features/SalesAgent/SalesAgentClient.swift`、`Features/AI/Networking/ManagerAgentClient.swift` | 已通过 Agent REST/SSE 调用,继续复用 |
|
||||
| xiaoqu-ai Android | `app/src/main/java/com/xiaoquai/crm/data/remote/WebSocketManager.kt`、`ApiService.kt` | 已通过 Gateway WebSocket/REST 调用,继续复用 |
|
||||
| xiaoqu-ai Web | `web/src/hooks/use-agent-chat.ts` | 已调用 Gateway `/api/v1/agents/{id}/invoke`,继续复用 |
|
||||
| xiaoqu-ai Agent | `agent-service`、`gateway` | 增加服务端 MCP Client 和用户 Token Provider |
|
||||
| 运营 iOS/Android | `ios`、`android` | 已调用 `ops-assistant-service` REST/WebSocket,继续复用 |
|
||||
| 运营 Agent | `services/ops-assistant-service` | 在 Executor 工具适配层增加 MCP Client |
|
||||
|
||||
xiaoqu-ai iOS 的 `ClawBridge.swift` 当前具备端侧 MCP 能力,但 Android 和 Web 没有。
|
||||
为了让三端真正共用一套,本次不把新的 xiaoqu MCP Token 接入 ClawBridge,统一走服务端 Agent。
|
||||
|
||||
## 3. 统一架构
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ xiaoqu-ai │
|
||||
│ iOS ─┐ │
|
||||
│ Android ──► Gateway ──► Agent Runtime ──┐ │
|
||||
│ Web ─┘ │ │
|
||||
└────────────────────────────────────────┼────────────┘
|
||||
│ MCP
|
||||
┌────────────────────────────────────────┼────────────┐
|
||||
│ xiaoqu-operations-ai │ │
|
||||
│ iOS ─┐ │ │
|
||||
│ Android ──► ops-assistant-service ─────┤ │
|
||||
└────────────────────────────────────────┼────────────┘
|
||||
▼
|
||||
xiaoqu-agent-service
|
||||
Token + Scope + Tool
|
||||
行级权限 + Audit
|
||||
```
|
||||
|
||||
### 3.1 职责
|
||||
|
||||
| 层 | 职责 |
|
||||
|---|---|
|
||||
| iOS/Android/Web | 登录、会话 UI、流式展示、确认交互;只携带产品登录凭证 |
|
||||
| 产品 Gateway | 验证登录态,注入可信用户/租户上下文,代理 Agent 请求 |
|
||||
| 产品 Agent Runtime | 编排 Agent、维护 capability allowlist、获取用户 MCP Token、调用 MCP |
|
||||
| xiaoqu-agent-service | 校验 MCP Token、过滤工具、执行行级权限、查询业务数据、记录审计 |
|
||||
|
||||
## 4. 认证与 Token
|
||||
|
||||
### 4.1 三类凭证
|
||||
|
||||
| 凭证 | 使用者 | 用途 |
|
||||
|---|---|---|
|
||||
| 产品登录 Token | iOS/Android/Web → 产品 Gateway | 证明当前产品用户身份 |
|
||||
| `X-Open-Key` | 产品 Agent Runtime → `/api/open/token/ensure` | 服务间获取用户 MCP Token |
|
||||
| 用户 MCP Token `xqak_*` | 产品 Agent Runtime → `/mcp` | 绑定 MCP 用户、scope、权限和审计 |
|
||||
|
||||
`X-Open-Key` 和 MCP Token 均只存在于受信任服务端。
|
||||
|
||||
### 4.2 用户映射
|
||||
|
||||
1. Gateway 验证产品登录 Token。
|
||||
2. Agent Runtime 从可信上下文取得产品用户、租户和数据范围。
|
||||
3. Agent Runtime 从受信任用户资料源取得手机号/邮箱。
|
||||
4. Agent Runtime 用固定 `scopeCode` 调用 `POST /api/open/token/ensure`。
|
||||
5. xiaoqu-agent-service 返回该 MCP 用户对应的 Token。
|
||||
6. Agent Runtime 使用此 Token 调用 `/mcp`。
|
||||
|
||||
禁止接受客户端或模型传入的手机号、邮箱、`user_id`、`tenant_id`、`scopeCode`
|
||||
覆盖可信上下文。scope 由服务端配置固定,启用前与管理后台实际配置核对。
|
||||
|
||||
### 4.3 服务端缓存与生命周期
|
||||
|
||||
- 缓存键至少包含:产品、租户、产品用户 ID、scope。
|
||||
- 切换账号或退出登录时,产品会话失效;不得让新用户复用旧用户 MCP Session。
|
||||
- MCP 返回 401 时,只清除当前用户缓存并重新 `ensure` 一次。
|
||||
- 只读调用可在重连后重试一次;有副作用调用不得自动重放。
|
||||
- 日志仅记录用户 ID、scope、工具名、状态和耗时,不记录凭证明文或手机号/邮箱明文。
|
||||
|
||||
## 5. xiaoqu-mcp 契约
|
||||
|
||||
### 5.1 `POST /api/open/token/ensure`
|
||||
|
||||
认证:`X-Open-Key: <server-only-key>`。
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"mobile": "<trusted-user-mobile>",
|
||||
"email": "<trusted-user-email>",
|
||||
"scopeCode": "<server-configured-scope>"
|
||||
}
|
||||
```
|
||||
|
||||
- `mobile`、`email` 至少一个,手机号优先。
|
||||
- 现有有效 Token:解密 `key_cipher` 返回同一明文。
|
||||
- 历史无密文 Token:禁用旧 Token,自动签发新 Token。
|
||||
- 不存在有效 Token:创建长期 Token;回收通过禁用实现。
|
||||
- 响应和审计不得记录 Token 明文。
|
||||
|
||||
错误语义:`400` 参数/scope 错误,`401` Open Key 无效,`403` Open API 未启用,
|
||||
`404` 用户不存在或不属于允许的企业账号。
|
||||
|
||||
### 5.2 `POST /mcp`
|
||||
|
||||
```http
|
||||
Authorization: Bearer <user-mcp-token>
|
||||
Content-Type: application/json
|
||||
Accept: application/json, text/event-stream
|
||||
```
|
||||
|
||||
调用顺序:`initialize` → `notifications/initialized` → `tools/list` → `tools/call`。
|
||||
初始化响应包含 Session ID 时,后续请求携带 `Mcp-Session-Id`。
|
||||
|
||||
权限同时在两个位置收口:
|
||||
|
||||
```text
|
||||
产品 Agent capability allowlist
|
||||
∩ MCP Token scope
|
||||
∩ 当前用户/租户/行级数据范围
|
||||
```
|
||||
|
||||
`tools/list` 按 scope 过滤;`tools/call` 再次强制校验,不能只依赖提示词或隐藏菜单。
|
||||
|
||||
## 6. 最小变更清单
|
||||
|
||||
### 6.1 xiaoqu-agent-service
|
||||
|
||||
`ensure`、Token 密文、Bearer 鉴权、scope 和审计已交付,保持协议兼容;本轮主要配合
|
||||
两个 Agent Runtime 联调,不为客户端增加新接口。
|
||||
|
||||
### 6.2 xiaoqu-ai
|
||||
|
||||
| 层 | 变更 |
|
||||
|---|---|
|
||||
| Gateway/Agent Runtime | 增加用户 MCP Token Provider 和 MCP HTTP Client;接入现有工具适配层 |
|
||||
| iOS | 保持现有 Agent REST/SSE 调用;不修改 Keychain 和 ClawBridge 的 MCP Token |
|
||||
| Android | 保持现有 Gateway REST/WebSocket 调用;`TokenManager` 只保存产品 Token |
|
||||
| Web | 保持现有 `/agents/{id}/invoke` 等 Agent API;浏览器不感知 MCP |
|
||||
|
||||
### 6.3 xiaoqu-operations-ai
|
||||
|
||||
在现有 `Router → Planner → Executor → Answer` 中,只修改 Executor 工具适配层:
|
||||
|
||||
```text
|
||||
validate capability
|
||||
→ trusted user context
|
||||
→ MCP Token Provider
|
||||
→ MCP tools/call
|
||||
→ result projection
|
||||
→ execution trace / Answer
|
||||
```
|
||||
|
||||
具体设置见 `xiaoqu-mcp-integration-setup.md`。
|
||||
|
||||
## 7. 开发顺序
|
||||
|
||||
| # | 工作 | 完成标准 |
|
||||
|---|---|---|
|
||||
| 1 | 确认两个产品的用户映射和实际 scopeCode | 不依赖客户端身份字段,scope 已在管理后台存在 |
|
||||
| 2 | xiaoqu-ai Agent Runtime 接入 Token Provider + MCP Client | iOS/Android/Web 调同一 Agent 能力并得到一致结果 |
|
||||
| 3 | ops-assistant-service 接入 Token Provider + MCP Client | 运营 iOS/Android 经同一服务端链路成功 |
|
||||
| 4 | 多端与跨用户安全回归 | 无 Token 泄露、串号、越权或副作用自动重放 |
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
- xiaoqu-ai 同一用户在 iOS、Android、Web 调用同一能力,工具结果和数据权限一致。
|
||||
- 三端请求只出现产品登录 Token,不出现 MCP Token 或 `X-Open-Key`。
|
||||
- 用户 A/B、不同租户、不同 scope 的 MCP Session 和缓存完全隔离。
|
||||
- 运营 iOS/Android 经 `ops-assistant-service` 调用,客户端同样不持有 MCP Token。
|
||||
- 无 scope 时 Planner/Executor 不能调用;绕过 `tools/list` 直接 `tools/call` 仍被服务端拒绝并审计。
|
||||
- MCP 401 只刷新当前用户;有副作用调用不自动重放。
|
||||
- Token 不进入 prompt、工具参数、trace、普通日志、崩溃信息和 Git。
|
||||
|
||||
## 9. 风险与回退
|
||||
|
||||
| 风险 | 应对 |
|
||||
|---|---|
|
||||
| 产品用户与 MCP 用户映射错误 | 只用 Gateway 可信身份查询手机号/邮箱;联调覆盖同名和缺失用户 |
|
||||
| 服务端缓存串用户 | 复合缓存键;登录切换和并发测试 |
|
||||
| 两个 Agent Runtime 暴露不同工具 | 各自维护明确 allowlist,并用同一 MCP schema 做契约测试 |
|
||||
| MCP 不可用 | Agent 返回稳定的工具不可用错误,不回退公共 Token 或数据库直连 |
|
||||
|
||||
回退只关闭各 Agent Runtime 的 MCP capability 开关,iOS、Android、Web 继续使用原有
|
||||
Agent 功能;不得回退为客户端共享固定 Token。
|
||||
@@ -1,156 +0,0 @@
|
||||
# xiaoqu-operations-ai 接入 xiaoqu-mcp 开发设置
|
||||
|
||||
## 1. 目标
|
||||
|
||||
让 `ops-assistant-service` 作为运营项目唯一 MCP Client 调用
|
||||
`xiaoqu_mcp_service`,同时服务运营 iOS、Android。保持现有:
|
||||
|
||||
```text
|
||||
Router → Planner → Executor → Answer
|
||||
```
|
||||
|
||||
每次调用使用当前已验证用户对应的 MCP Token,不把 MCP Token 放进提示词、Planner 输出、工具参数或普通日志。
|
||||
|
||||
客户端边界:
|
||||
|
||||
| 端 | 调用入口 | MCP Token |
|
||||
|---|---|---|
|
||||
| 运营 iOS | `ops-assistant-service` REST/WebSocket | 不下发 |
|
||||
| 运营 Android | `ops-assistant-service` REST/WebSocket | 不下发 |
|
||||
|
||||
客户端只持产品登录凭证;`ops-assistant-service` 从已验证身份映射用户后,服务端获取并使用 MCP Token。
|
||||
|
||||
## 2. 当前代码结论
|
||||
|
||||
- `ops-assistant-service` 是 FastAPI + Python 3.12 服务。
|
||||
- 当前依赖中没有 MCP SDK,也没有 `tools/list` / `tools/call` 客户端。
|
||||
- `Executor` 目前直接调用本地 capability:`customer_issues.*`、`smartclean.query`。
|
||||
- 用户身份来自 Gateway JWT,并通过 `ops_identity_mappings` 解析租户、用户和 `scope_tags`。
|
||||
- 当前最合适的接入点是 Executor 的工具适配层,不新增第二套编排器。
|
||||
|
||||
## 3. xiaoqu-mcp 服务契约
|
||||
|
||||
### 3.1 MCP 调用
|
||||
|
||||
- 地址:`POST <XQ_MCP_URL>/mcp`
|
||||
- 认证:`Authorization: Bearer <xqak-token>`
|
||||
- 初始化顺序:`initialize` → `notifications/initialized` → `tools/list` → `tools/call`
|
||||
- 初始化后保存 `Mcp-Session-Id`。
|
||||
- `tools/list` 已按 Token scope 过滤,`tools/call` 仍由服务端再次鉴权。
|
||||
|
||||
### 3.2 Token 初始化
|
||||
|
||||
服务间调用:`POST <XQ_MCP_URL>/api/open/token/ensure`
|
||||
|
||||
```http
|
||||
X-Open-Key: <server-only-key>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体包含 `mobile` 或 `email`,以及固定的 `scopeCode`(首期建议 `operation`)。
|
||||
|
||||
约束:
|
||||
|
||||
- `X-Open-Key` 只能配置在受信任后端,不能下发 App。
|
||||
- 业务后端必须从已验证用户会话查询手机号/邮箱,不能信任客户端或模型传入的身份字段。
|
||||
- 首次创建返回 Token 明文;xiaoqu-mcp 同时保存 SHA-256 哈希和 AES-GCM 加密密文(密钥由 `app.token-cipher-key` 派生)。
|
||||
- 已存在 Token 解密回传明文(二次获取),重复调用幂等复用同一把 Token;历史无密文 Token 自动轮换(禁用旧把、签发新把)。
|
||||
- Token 密文解密失败必须报错;不得回退到哈希值或公共 Token。
|
||||
|
||||
## 4. 推荐实现
|
||||
|
||||
### 4.1 配置
|
||||
|
||||
在 `config.py` 增加以下 `OPS_ASSISTANT_` 配置:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `XQ_MCP_ENABLED` | `false` | 是否启用 MCP capability |
|
||||
| `XQ_MCP_URL` | unset | 例如 `http://xiaoqu-mcp:8088`,只存地址,不存用户 Token |
|
||||
| `XQ_MCP_OPEN_KEY` | unset | 仅用于服务端 Token 初始化,使用 `SecretStr` |
|
||||
| `XQ_MCP_SCOPE_CODE` | `operation` | 固定作用域,禁止由 Planner 或客户端选择 |
|
||||
| `XQ_MCP_TIMEOUT_SECONDS` | `10` | MCP 请求超时 |
|
||||
|
||||
启用时必须同时提供 URL、Open Key 和 scope;生产环境禁止明文写入仓库。
|
||||
|
||||
### 4.2 新增最小模块
|
||||
|
||||
建议只增加两个模块:
|
||||
|
||||
- `src/ops_assistant_service/mcp_client.py`
|
||||
- 管理 HTTP 请求、JSON-RPC、Session ID、Bearer Header、超时和错误映射。
|
||||
- 进程内按 `user_id` 缓存 MCP Session;账号或 Token 变化时销毁。
|
||||
- `src/ops_assistant_service/mcp_token_provider.py`
|
||||
- 接收已验证的 `SharedIdentity`。
|
||||
- 从受信任用户资料源取得 mobile/email。
|
||||
- 调用 `ensure`,首次签发返回明文;后续直接复用本地持久化 token。
|
||||
- 不接受 `user_id/mobile/email` 作为客户端覆盖参数。
|
||||
|
||||
不引入通用 MCP 框架、第二个 Agent 或数据库直连;现有 `httpx` 已足够完成首期只读接入。
|
||||
|
||||
### 4.3 Capability 映射
|
||||
|
||||
在 `capability_registry.py` 增加固定的本地 capability,禁止把远端 `tools/list` 原样暴露给 Planner:
|
||||
|
||||
| 本地 capability | MCP 工具 | 首期权限 |
|
||||
|---|---|---|
|
||||
| `xiaoqu.agent_count` | `agent.count` | read |
|
||||
| `xiaoqu.agent_list` | `agent.list.query` | read |
|
||||
| `xiaoqu.property_list` | `property.list.query` | read |
|
||||
|
||||
Planner 只能选择注册表中的名称和 schema。Executor 在调用前同时检查:
|
||||
|
||||
```text
|
||||
应用允许 ∩ 用户 scope_tags ∩ 租户/数据范围
|
||||
```
|
||||
|
||||
远端返回结果先经过字段白名单投影,再交给 Answer;Token、原始 Header 和未授权字段不得进入 trace。
|
||||
|
||||
### 4.4 Executor 接入点
|
||||
|
||||
在 `executor.py` 增加一个只读分支:
|
||||
|
||||
```text
|
||||
validate_tool_input
|
||||
→ 校验本地 capability 和参数白名单
|
||||
→ 读取 SharedIdentity / DataScope
|
||||
→ 获取当前用户 MCP Token
|
||||
→ MCP tools/call
|
||||
→ 结果投影
|
||||
→ 现有 execution_trace / Answer 链路
|
||||
```
|
||||
|
||||
现有 `customer_issues.*` 和 `smartclean.query` 保持不变。MCP 不可用时返回稳定的 `TOOL_UNAVAILABLE`,不得回退到公共 Token 或直接访问 xiaoqu-mcp 数据库。
|
||||
|
||||
## 5. 生命周期与安全
|
||||
|
||||
- 登录用户变化:先关闭旧 Session,再建立新用户 Session。
|
||||
- MCP 返回 401:清理当前用户 Token,最多执行一次重新获取和重连;不自动重放副作用请求。
|
||||
- 退出登录:清理本地/会话 Token 缓存;如需远端立即失效,走服务端管理端吊销能力。
|
||||
- 日志只记录用户 ID、工具名、scope 和错误码,不记录 Token、Authorization Header 或手机号/邮箱明文。
|
||||
- `tools/list` 仅用于校验远端能力;真正可调用集合以本地 allowlist 为准。
|
||||
|
||||
## 6. 开发顺序
|
||||
|
||||
1. 先确认 xiaoqu-mcp 的 `ensure` 契约和 Token 密文落库;运营链路不依赖 xiaoqu-ai 业务后端门面。
|
||||
2. 增加 `Settings` 配置和启动校验。
|
||||
3. 实现 `mcp_token_provider.py` 和 `mcp_client.py`,补充 JSON-RPC/Session 单元测试。
|
||||
4. 增加 3 个只读 capability,并接入 Executor。
|
||||
5. 增加用户隔离、scope 拒绝、401 重连、Token 脱敏和 MCP 不可用测试。
|
||||
6. 先在 development/test 环境启用,完成联调后再进入 staging。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
- 用户 A 的请求始终使用 A 的 MCP Token,用户 B 不能复用。
|
||||
- A 无对应 scope 时,Planner/Executor 均不能调用该 MCP 工具。
|
||||
- `tools/list`、`tools/call` 使用同一用户 Session 和 Bearer Token。
|
||||
- 切换用户后旧 Session 不再发出请求。
|
||||
- MCP 401 只刷新当前用户,且同一请求不重复执行副作用。
|
||||
- 测试、日志、trace、配置文件和 Git 历史中均无 Token 明文。
|
||||
|
||||
## 8. 当前不做
|
||||
|
||||
- 不把 MCP Client 放进 iOS/Android 端。
|
||||
- 不把全部远程工具动态注册给 Planner。
|
||||
- 不复制 xiaoqu-mcp 数据库或绕过 MCP 权限。
|
||||
- 不在本次首期引入写工具;写工具需另行设计审批、幂等和补偿协议。
|
||||
@@ -0,0 +1,137 @@
|
||||
# xiaoqu 多产品 Agent 统一接入 xiaoqu-mcp 总体设计
|
||||
|
||||
## 1. 设计结论
|
||||
|
||||
xiaoqu-ai 与 xiaoqu-operations-ai 共用 xiaoqu-mcp 的服务能力、Token 原语、工具协议、权限模型和审计规则,但保留各自的 Agent Runtime:
|
||||
|
||||
```text
|
||||
xiaoqu-ai iOS / Android / Web
|
||||
│ 产品登录 Token
|
||||
▼
|
||||
xiaoqu-ai Gateway / Agent Runtime ───┐
|
||||
│ 用户 MCP Token(仅服务端持有)
|
||||
xiaoqu-operations-ai iOS / Android / Web
|
||||
│ 产品登录 Token │
|
||||
▼ │
|
||||
ops-assistant-service ───────────────┤
|
||||
▼
|
||||
xiaoqu-agent-service /mcp
|
||||
Token + Scope + Tool
|
||||
行级权限 + Audit
|
||||
```
|
||||
|
||||
共同的是后端协议,不是 Swift、Kotlin 或 Web UI 代码。任何客户端都不实现本次 xiaoqu-mcp Client,不保存用户 MCP Token,也不接触 `X-Open-Key`。
|
||||
|
||||
## 2. 范围与文档分工
|
||||
|
||||
| 层级 | 项目 | 项目目录 | 文档 |
|
||||
|---|---|---|---|
|
||||
| 总体 | xiaoqu-mcp | 当前仓库 `xiaoqu-agent-service` | 本文 |
|
||||
| 详细设计 | xiaoqu-ai iOS、Android、Web、服务端 Agent | `/Users/tonylai/xiaoquai/xiaoqu-ai`、`/Users/tonylai/jiaxk/xiaoqu-ai-android` | [`xiaoqu-ai-mcp-integration-design.md`](./xiaoqu-ai-mcp-integration-design.md) |
|
||||
| 详细设计 | xiaoqu-operations-ai iOS、Android、Web、服务端 Agent | `/Users/tonylai/wangheng/xiaoqu-operations-ai` | [`xiaoqu-operations-ai-mcp-integration-design.md`](./xiaoqu-operations-ai-mcp-integration-design.md) |
|
||||
|
||||
本文只定义两个产品必须共同遵守的架构和安全契约。项目代码落点、配置、Capability 映射和项目级验收只写在对应详细设计中。
|
||||
|
||||
## 3. 统一职责
|
||||
|
||||
| 层 | 职责 |
|
||||
|---|---|
|
||||
| iOS / Android / Web | 登录、会话 UI、流式展示和确认交互;只携带产品登录凭证 |
|
||||
| 产品 Gateway | 验证登录态,建立可信用户、租户和数据范围上下文 |
|
||||
| 产品 Agent Runtime | 编排 Agent,维护本地 Capability allowlist,获取用户 MCP Token 并调用 MCP |
|
||||
| xiaoqu-agent-service | 校验 MCP Token,按 scope 过滤工具,执行用户/租户/行级权限并记录审计 |
|
||||
|
||||
两个产品可以暴露不同的 Capability 集合,但都必须通过同一套 MCP 鉴权和工具契约执行。运营项目不得直接调用 xiaoqu-ai Agent,也不得复制或直连 xiaoqu-mcp 数据库。
|
||||
|
||||
## 4. 认证与 Token 契约
|
||||
|
||||
### 4.1 三类凭证
|
||||
|
||||
| 凭证 | 使用链路 | 用途 |
|
||||
|---|---|---|
|
||||
| 产品登录 Token | 客户端 → 产品 Gateway | 证明当前产品用户身份 |
|
||||
| `X-Open-Key` | 产品 Agent Runtime → `/api/open/token/ensure` | 服务间获取指定用户的 MCP Token |
|
||||
| 用户 MCP Token `xqak_*` | 产品 Agent Runtime → `/mcp` | 绑定 MCP 用户、scope、权限和审计 |
|
||||
|
||||
`X-Open-Key` 和用户 MCP Token 只能存在于受信任服务端,不得进入客户端、prompt、模型输出、工具参数、普通日志或 Git。
|
||||
|
||||
### 4.2 用户映射
|
||||
|
||||
1. Gateway 验证产品登录 Token。
|
||||
2. Agent Runtime 从可信上下文取得产品用户、租户和数据范围。
|
||||
3. Agent Runtime 从受信任用户资料源取得手机号或邮箱。
|
||||
4. Agent Runtime 使用服务端固定的 `scopeCode` 调用 `POST /api/open/token/ensure`。
|
||||
5. xiaoqu-agent-service 返回该 MCP 用户对应的 Token。
|
||||
6. Agent Runtime 使用该 Token 调用 `/mcp`。
|
||||
|
||||
客户端、Planner 和模型不得提供或覆盖 `user_id`、手机号、邮箱、租户、数据范围或 `scopeCode`。每个产品启用前必须核对管理后台中的真实 scope 和工具绑定。
|
||||
|
||||
### 4.3 `POST /api/open/token/ensure`
|
||||
|
||||
认证:`X-Open-Key: <server-only-key>`。
|
||||
|
||||
```json
|
||||
{
|
||||
"mobile": "<trusted-user-mobile>",
|
||||
"email": "<trusted-user-email>",
|
||||
"scopeCode": "<server-configured-scope>"
|
||||
}
|
||||
```
|
||||
|
||||
- `mobile`、`email` 至少一个,手机号优先。
|
||||
- 已有有效 Token:解密 `key_cipher` 并返回同一明文。
|
||||
- 历史无密文 Token:禁用旧 Token 并自动签发新 Token。
|
||||
- 不存在有效 Token:创建长期 Token;回收通过禁用实现。
|
||||
- 密文解密失败必须报错,不得回退哈希值或公共 Token。
|
||||
|
||||
错误语义:`400` 参数或 scope 错误,`401` Open Key 无效,`403` Open API 未启用,`404` 用户不存在或不属于允许的企业账号。
|
||||
|
||||
## 5. MCP 协议与权限
|
||||
|
||||
调用入口:`POST /mcp`。
|
||||
|
||||
```http
|
||||
Authorization: Bearer <user-mcp-token>
|
||||
Content-Type: application/json
|
||||
Accept: application/json, text/event-stream
|
||||
```
|
||||
|
||||
调用顺序:`initialize` → `notifications/initialized` → `tools/list` → `tools/call`。初始化响应包含 Session ID 时,后续请求携带 `Mcp-Session-Id`。
|
||||
|
||||
最终可执行能力为:
|
||||
|
||||
```text
|
||||
产品 Agent capability allowlist
|
||||
∩ MCP Token scope
|
||||
∩ 当前用户权限
|
||||
∩ 当前租户/数据范围
|
||||
```
|
||||
|
||||
`tools/list` 必须按 Token scope 过滤,`tools/call` 必须再次强制校验。不得仅依赖提示词、本地隐藏菜单或 Planner 输出做授权。
|
||||
|
||||
## 6. 生命周期与错误处理
|
||||
|
||||
- 缓存键至少包含产品、租户、产品用户 ID 和 scope。
|
||||
- 登录用户变化或退出登录时,旧用户的 Token 缓存和 MCP Session 必须失效。
|
||||
- MCP 返回 401 时,只清理当前用户缓存并重新 `ensure` 一次。
|
||||
- 只读调用可在重新连接后重试一次;有副作用调用不得自动重放。
|
||||
- MCP 不可用时返回稳定的工具不可用错误,不得回退公共 Token 或数据库直连。
|
||||
- 日志只记录用户 ID、scope、工具名、状态和耗时,不记录凭证明文或手机号/邮箱明文。
|
||||
|
||||
## 7. 联调顺序
|
||||
|
||||
| # | 工作 | 完成标准 |
|
||||
|---|---|---|
|
||||
| 1 | 确认两个产品的用户映射与实际 scope | scope 已存在并绑定所需工具,不依赖客户端身份字段 |
|
||||
| 2 | 完成 xiaoqu-ai 服务端接入 | iOS、Android、Web 经同一服务端适配器调用 |
|
||||
| 3 | 完成 xiaoqu-operations-ai 服务端接入 | 运营 iOS、Android、Web 经 ops-assistant-service 调用 |
|
||||
| 4 | 跨用户、跨租户和跨产品回归 | 无 Token 泄露、串号、越权或副作用自动重放 |
|
||||
|
||||
## 8. 总体验收与回退
|
||||
|
||||
- 所有客户端请求只出现产品登录 Token,不出现用户 MCP Token 或 `X-Open-Key`。
|
||||
- 用户 A/B、不同租户、不同 scope 的 Token 缓存和 MCP Session 完全隔离。
|
||||
- 绕过 `tools/list` 直接调用 `tools/call` 仍会被服务端拒绝并审计。
|
||||
- 两个产品的本地 allowlist 可以不同,但同名 MCP 工具的请求和响应契约一致。
|
||||
- 任一产品回退时,只关闭该 Agent Runtime 的 MCP Capability 开关;客户端继续使用原有 Agent 功能。
|
||||
- 禁止回退为客户端共享固定 Token、公共服务端 Token 或数据库直连。
|
||||
@@ -0,0 +1,181 @@
|
||||
# xiaoqu-operations-ai 接入 xiaoqu-mcp 详细设计
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
本文是 [`xiaoqu 多产品 Agent 统一接入 xiaoqu-mcp 总体设计`](./xiaoqu-mcp-multi-product-agent-design.md) 的运营项目落地设计,代码基线为 `/Users/tonylai/wangheng/xiaoqu-operations-ai`。
|
||||
|
||||
运营 iOS、Android、Web 共用 `ops-assistant-service` 中的一套 MCP Client:
|
||||
|
||||
```text
|
||||
iOS / Android / Web
|
||||
│ 产品登录 Token
|
||||
▼
|
||||
ops-assistant-service REST
|
||||
│
|
||||
▼
|
||||
Router → Resolver → Planner → Executor → Answer
|
||||
│
|
||||
▼
|
||||
Capability Registry
|
||||
│ 固定 adapter_id
|
||||
▼
|
||||
xiaoqu-mcp Adapter
|
||||
│ 用户 MCP Token(仅服务端持有)
|
||||
▼
|
||||
xiaoqu-agent-service /mcp
|
||||
```
|
||||
|
||||
WebSocket 只负责会话进度和完成通知,不承载 MCP 调用。客户端不新增 MCP 代码,不保存用户 MCP Token、MCP Session ID 或 `X-Open-Key`。
|
||||
|
||||
## 2. 当前代码依据
|
||||
|
||||
- `web/ops-assistant/src/api.ts` 通过 `/api/v1/ops-assistant/*` REST 提交消息和读取业务数据。
|
||||
- `web/ops-assistant/src/ConversationPage.tsx` 使用 WebSocket 订阅会话进度,消息提交仍走 REST。
|
||||
- `services/ops-assistant-service` 是 FastAPI + Python 3.12 服务,现有 `httpx` 足够实现 MCP HTTP Client。
|
||||
- `worker.py` 同时运行旧流水线和 `contextual_fulfillment_v2`;新流水线包含 Router、Resolver、Planner、Recovery、Executor、Answer。
|
||||
- `capability_registry.py` 维护静态 `CAPABILITY_REGISTRY`、冻结的 Capability Contract 和 `adapter_id` 白名单。
|
||||
- 旧 `executor.py` 与新 `contextual_execution.py` 最终都调用 `execute_registered_tool()`。
|
||||
- 当前 Registry 包含 `customer_issues.*`、`vision_analyze`、`work_items.list`、`read_only_todos.list`、`knowledge_entries.search`,已不包含 `smartclean.query`。
|
||||
- Gateway JWT 在入口解析为共享身份,再通过 `ops_identity_mappings` 映射为运营 `tenant_id`、`user_id` 和 DataScope;后台 Worker 执行时使用运营 UUID。
|
||||
|
||||
因此不在 Web、路由层或某一条流水线中增加专用 MCP 分支。唯一接入点是两套流水线共用的 Capability Adapter 执行边界。
|
||||
|
||||
## 3. 设计决策
|
||||
|
||||
### 3.1 统一 Adapter
|
||||
|
||||
在 `CAPABILITY_REGISTRY` 中注册稳定的本地 Capability 和固定 `adapter_id`,由 `execute_registered_tool()` 将 MCP Adapter ID 路由到 `XiaoquMcpAdapter`。
|
||||
|
||||
```text
|
||||
Planner 选择本地 Capability
|
||||
→ 校验冻结的 Capability Contract
|
||||
→ 校验输入 schema、scope_tags、DataScope
|
||||
→ execute_registered_tool(adapter_id)
|
||||
→ XiaoquMcpAdapter
|
||||
→ MCP tools/call
|
||||
```
|
||||
|
||||
不得把远端 `tools/list` 原样注册给 Planner。`tools/list` 只用于启动或联调时验证远端工具是否与本地冻结契约一致。
|
||||
|
||||
### 3.2 显式依赖传递
|
||||
|
||||
`XiaoquMcpRuntime` 由 `worker.build_pipeline_runtime()` 创建,并通过显式参数传给旧 `run_executor_tick` 和新 `run_contextual_executor_tick`。禁止用模块级全局变量或从请求上下文隐式读取用户。
|
||||
|
||||
`PipelineRuntime.close()` 负责关闭 MCP HTTP Client 和清理进程内 Session 缓存。未启用 MCP 时传入 `None`,现有本地 Capability 保持原行为。
|
||||
|
||||
## 4. 配置设计
|
||||
|
||||
在 `src/ops_assistant_service/config.py` 的 `Settings` 增加以下 `OPS_ASSISTANT_` 配置:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `XQ_MCP_ENABLED` | `false` | 是否启用 MCP Capability |
|
||||
| `XQ_MCP_URL` | unset | xiaoqu-mcp 内网地址 |
|
||||
| `XQ_MCP_OPEN_KEY` | unset | Token 初始化凭证,使用 `SecretStr` |
|
||||
| `XQ_MCP_SCOPE_CODE` | `operation` | 服务端固定 scope,禁止由客户端或模型选择 |
|
||||
| `XQ_MCP_TIMEOUT_SECONDS` | `10` | MCP 请求超时 |
|
||||
|
||||
启用时 URL、Open Key 和 scope 必填。`operation` 已存在于 xiaoqu-mcp dev 种子;development、test、staging、production 均须分别核对真实 scope 和工具绑定。凭证只由部署 Secret 注入。
|
||||
|
||||
## 5. 最小模块
|
||||
|
||||
### 5.1 `mcp_token_provider.py`
|
||||
|
||||
输入为 Worker 已持有的运营 `tenant_id`、`user_id` 和固定 scope:
|
||||
|
||||
1. 从 `ops_identity_mappings` 反查可信的 `shared_tenant_id`、`shared_user_id`。
|
||||
2. 通过现有只读共享用户 Port 获取权威 mobile;权威数据源存在 email 时可作为备用。
|
||||
3. 使用固定 `scopeCode` 调用 `/api/open/token/ensure`。
|
||||
4. 按运营租户、运营用户、共享用户和 scope 缓存 Token。
|
||||
|
||||
现有 `SharedUserPort` 尚未暴露 mobile/email,需要最小扩展其只读 DTO 和 PostgreSQL Adapter;不得复制用户表,也不得使用 Web、Planner、工具参数或模型输出中的身份字段。
|
||||
|
||||
### 5.2 `mcp_client.py`
|
||||
|
||||
使用现有 `httpx.Client` 管理:
|
||||
|
||||
- `initialize`、`notifications/initialized`、`tools/list`、`tools/call`。
|
||||
- Bearer Header、`Mcp-Session-Id`、JSON-RPC ID 和响应校验。
|
||||
- 连接、读取、写入和总超时。
|
||||
- 按运营租户、运营用户和 scope 隔离 Session。
|
||||
- 关闭时释放 HTTP Client,不持久化 MCP Session。
|
||||
|
||||
### 5.3 `mcp_adapter.py`
|
||||
|
||||
`XiaoquMcpAdapter` 只接收已经验证的 Capability Contract、运营身份和参数:
|
||||
|
||||
- 根据固定映射选择远端 MCP 工具。
|
||||
- 调用 Token Provider 和 MCP Client。
|
||||
- 校验远端响应并执行字段白名单投影。
|
||||
- 返回现有 Executor/Answer 可消费的字典,不返回 Token、Header 或原始未授权字段。
|
||||
|
||||
不引入通用 MCP 框架或第二套 Agent 编排器。
|
||||
|
||||
## 6. Capability 映射
|
||||
|
||||
首期只注册只读 Capability:
|
||||
|
||||
| 本地 Capability | `adapter_id` | MCP 工具 | 权限 |
|
||||
|---|---|---|---|
|
||||
| `xiaoqu.agent_count` | `mcp.xiaoqu.agent_count.v1` | `agent.count` | read |
|
||||
| `xiaoqu.agent_list` | `mcp.xiaoqu.agent_list.v1` | `agent.list.query` | read |
|
||||
| `xiaoqu.property_list` | `mcp.xiaoqu.property_list.v1` | `property.list.query` | read |
|
||||
|
||||
每项必须在 `CAPABILITY_REGISTRY` 中声明输入/输出 schema、风险、审批、超时重试、审计事件和结果模板,并将精确 `adapter_id` 加入 `EXECUTION_ADAPTER_IDS`。已有本地 Capability 不改名、不改 Adapter、不改变执行语义。
|
||||
|
||||
## 7. 身份与权限
|
||||
|
||||
最终授权取交集:
|
||||
|
||||
```text
|
||||
本地 Capability allowlist
|
||||
∩ 当前运营用户 scope_tags / DataScope
|
||||
∩ MCP Token scope
|
||||
∩ xiaoqu-mcp 用户、租户和行级数据范围
|
||||
```
|
||||
|
||||
- REST 和 WebSocket 只负责验证产品登录态并建立运营身份。
|
||||
- Planner 只能选择 Registry 中的 Capability,不决定用户、租户或 scope。
|
||||
- Worker 必须在每次执行前确认 `ops_identity_mappings` 仍有效,不能仅依赖消息入库时的旧快照。
|
||||
- `tools/list` 与 `tools/call` 使用同一用户 Token 和 MCP Session;服务端在两个入口都强制鉴权。
|
||||
|
||||
## 8. 异步执行与生命周期
|
||||
|
||||
Web、iOS、Android 提交消息后获得 `202`;Worker 在后台完成 Agent 和工具执行,再由消息查询或 WebSocket 返回进度与结果。
|
||||
|
||||
- `build_pipeline_runtime()` 创建一个进程级 `XiaoquMcpRuntime`,但 Token 和 Session 按用户隔离。
|
||||
- Worker 重启后 Session 不恢复;首次调用重新 `ensure` 和 `initialize`。
|
||||
- 用户映射失效、scope 版本变化或 MCP 401 时,只清理该用户缓存。
|
||||
- MCP 401:重新 `ensure` 和连接一次;只读调用最多重试一次。
|
||||
- MCP 403:直接返回权限不足,不刷新 Token。
|
||||
- 超时、协议错误或服务不可用:映射为稳定的 `TOOL_UNAVAILABLE`,并让当前 Execution Attempt 进入可审计失败状态。
|
||||
- 不自动重放写操作;首期不接写工具。
|
||||
|
||||
## 9. 开发顺序
|
||||
|
||||
1. 核对各环境 `operation` scope、三项工具绑定和用户映射数据源。
|
||||
2. 扩展只读 SharedUser Port,完成运营 UUID 到 mobile/email 的可信解析测试。
|
||||
3. 增加 Settings 配置、启用校验和 `XiaoquMcpRuntime` 生命周期。
|
||||
4. 实现 Token Provider、MCP Client 和 Adapter 的协议单元测试。
|
||||
5. 注册三项本地 Capability,并让旧 Executor 与 contextual v2 共用同一 Adapter。
|
||||
6. 覆盖用户隔离、租户隔离、scope 拒绝、401、403、超时、脱敏和 Worker 重启测试。
|
||||
7. 分别从 iOS、Android、Web 提交对话请求,验证异步进度和最终结果一致。
|
||||
|
||||
## 10. 项目验收
|
||||
|
||||
- iOS、Android、Web 均只调用 `ops-assistant-service`,客户端没有 MCP Token、Session ID 或 Open Key。
|
||||
- Web 消息走 REST,WebSocket 仅订阅进度/完成通知。
|
||||
- 旧流水线和 `contextual_fulfillment_v2` 对同一 MCP Capability 使用同一个 Adapter。
|
||||
- 用户 A/B、不同租户和 scope 的 Token 与 MCP Session 不互用。
|
||||
- Planner 不能选择未注册工具;冻结 Contract、`scope_tags`、DataScope 或 MCP scope 任一不满足即拒绝。
|
||||
- 现有七项本地 Capability 的契约和行为不受影响。
|
||||
- MCP 401 只刷新当前用户;MCP 不可用时不回退公共 Token、数据库直连或 xiaoqu-ai Agent。
|
||||
- 测试、日志、trace、配置和 Git 历史中无凭证明文。
|
||||
|
||||
## 11. 当前不做
|
||||
|
||||
- 不在运营 iOS、Android、Web 中实现 MCP Client。
|
||||
- 不动态注册全部远程工具。
|
||||
- 不恢复或假定已删除的 `smartclean.query`。
|
||||
- 不复制 xiaoqu-mcp 或共享用户数据库。
|
||||
- 不在首期引入写工具;写工具需另行审批幂等、确认和补偿协议。
|
||||
@@ -5,6 +5,7 @@ import { api, setCsrf } from './api'
|
||||
|
||||
const menus = [
|
||||
{ id: 'tokens', label: '用户与 Token', icon: '👥' },
|
||||
{ id: 'clients', label: '服务密钥', icon: '🔐' },
|
||||
{ id: 'tools', label: '工具管理', icon: '🔧' },
|
||||
{ id: 'scopes', label: '作用域管理', icon: '🧩' },
|
||||
{ id: 'audit', label: '审计日志', icon: '📋' },
|
||||
@@ -20,6 +21,7 @@ const userPage = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const selectedUser = ref(null)
|
||||
const permission = ref(null)
|
||||
const tokens = ref([])
|
||||
const openClients = ref([])
|
||||
const scopes = ref([])
|
||||
const tools = ref([])
|
||||
const audits = ref([])
|
||||
@@ -28,6 +30,9 @@ const auditFilter = reactive({ person: '', tokenPrefix: '', tool: '', result: ''
|
||||
const tokenDialog = ref(false)
|
||||
const tokenForm = reactive({ id: null, memo: '', scopeIds: [], scopeId: null, expireTime: null })
|
||||
const issuedTokens = ref([])
|
||||
const clientDialog = ref(false)
|
||||
const clientForm = reactive({ id: null, name: '', memo: '', scopeIds: [] })
|
||||
const issuedOpenKey = ref(null)
|
||||
const scopeDialog = ref(false)
|
||||
const scopeForm = reactive({ id: null, name: '', code: '', memo: '' })
|
||||
const scopeToolsDialog = ref(false)
|
||||
@@ -41,6 +46,20 @@ const groupedTools = computed(() => tools.value.reduce((groups, tool) => {
|
||||
;(groups[category] ||= []).push(tool)
|
||||
return groups
|
||||
}, {}))
|
||||
const issuedKeyCurl = computed(() => {
|
||||
if (!issuedOpenKey.value) return ''
|
||||
const payload = JSON.stringify({
|
||||
scopeCode: 'sync', principalType: 'service', serviceName: issuedOpenKey.value.client.name,
|
||||
})
|
||||
return [
|
||||
"curl -X POST 'https://agent.ququclean.com/api/open/token/ensure' \\",
|
||||
` -H 'X-Open-Key: ${issuedOpenKey.value.plaintext}' \\`,
|
||||
" -H 'Content-Type: application/json' \\",
|
||||
` -d ${shellQuote(payload)}`,
|
||||
].join('\n')
|
||||
})
|
||||
|
||||
function shellQuote(value) { return `'${value.replaceAll("'", `'"'"'`)}'` }
|
||||
|
||||
function qs(values) {
|
||||
const params = new URLSearchParams()
|
||||
@@ -76,6 +95,7 @@ async function logout() {
|
||||
async function switchMenu(id) {
|
||||
current.value = id
|
||||
if (id === 'tokens') await loadUsers()
|
||||
if (id === 'clients') await loadOpenClients()
|
||||
if (id === 'tools') await loadTools()
|
||||
if (id === 'scopes') await loadScopes()
|
||||
if (id === 'audit') await Promise.all([loadAudit(), api('/tools').then(data => { tools.value = data })])
|
||||
@@ -146,6 +166,54 @@ async function deleteToken(token) {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function loadOpenClients() {
|
||||
;[openClients.value, scopes.value] = await Promise.all([api('/open-clients'), api('/scopes')])
|
||||
}
|
||||
|
||||
function openClient(client = null) {
|
||||
Object.assign(clientForm, client ? {
|
||||
id: client.id, name: client.name, memo: client.memo || '', scopeIds: [...client.scopeIds],
|
||||
} : { id: null, name: '', memo: '', scopeIds: [] })
|
||||
clientDialog.value = true
|
||||
}
|
||||
|
||||
async function saveClient() {
|
||||
if (!clientForm.name.trim()) return ElMessage.warning('请填写调用方名称')
|
||||
if (!clientForm.scopeIds.length) return ElMessage.warning('至少选择一个作用域')
|
||||
await run(async () => {
|
||||
const body = JSON.stringify({ name: clientForm.name, memo: clientForm.memo, scopeIds: clientForm.scopeIds })
|
||||
if (clientForm.id) await api(`/open-clients/${clientForm.id}`, { method: 'PUT', body })
|
||||
else issuedOpenKey.value = await api('/open-clients', { method: 'POST', body })
|
||||
clientDialog.value = false
|
||||
await loadOpenClients()
|
||||
ElMessage.success(clientForm.id ? '服务密钥配置已保存' : '服务密钥已创建')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function setClientStatus(client) {
|
||||
if (client.status === 1) {
|
||||
try { await ElMessageBox.confirm(`吊销后 ${client.name} 将立即无法用此密钥换取 Token,确认继续?`, '吊销服务密钥', { type: 'warning' }) }
|
||||
catch { return }
|
||||
}
|
||||
await run(async () => {
|
||||
await api(`/open-clients/${client.id}/status`, {
|
||||
method: 'PATCH', body: JSON.stringify({ status: client.status === 1 ? 0 : 1 }),
|
||||
})
|
||||
await loadOpenClients()
|
||||
ElMessage.success(client.status === 1 ? '服务密钥已吊销' : '服务密钥已启用')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function rotateClient(client) {
|
||||
try { await ElMessageBox.confirm(`轮换后 ${client.name} 的旧密钥立即失效,确认继续?`, '轮换服务密钥', { type: 'warning' }) }
|
||||
catch { return }
|
||||
await run(async () => {
|
||||
issuedOpenKey.value = await api(`/open-clients/${client.id}/rotate`, { method: 'POST' })
|
||||
await loadOpenClients()
|
||||
ElMessage.success('服务密钥已轮换')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function loadTools() {
|
||||
tools.value = await api(`/tools${qs(toolFilter)}`)
|
||||
}
|
||||
@@ -212,6 +280,7 @@ async function copyText(value) { await navigator.clipboard.writeText(value); ElM
|
||||
async function copyAllTokens() {
|
||||
await copyText(issuedTokens.value.map(item => `${item.token.scopeNames.join('/')}: ${item.plaintext}`).join('\n'))
|
||||
}
|
||||
function clearIssuedOpenKey() { issuedOpenKey.value = null }
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -242,6 +311,7 @@ onMounted(async () => {
|
||||
<div class="logo"><span>趣</span> 智能体服务后台</div>
|
||||
<nav aria-label="主导航">
|
||||
<button :class="['menu-item', { active: current === 'tokens' }]" data-testid="admin-menu-tokens" @click="switchMenu('tokens')">👥 用户与 Token</button>
|
||||
<button :class="['menu-item', { active: current === 'clients' }]" data-testid="admin-menu-clients" @click="switchMenu('clients')">🔐 服务密钥</button>
|
||||
<button :class="['menu-item', { active: current === 'tools' }]" data-testid="admin-menu-tools" @click="switchMenu('tools')">🔧 工具管理</button>
|
||||
<button :class="['menu-item', { active: current === 'scopes' }]" data-testid="admin-menu-scopes" @click="switchMenu('scopes')">🧩 作用域管理</button>
|
||||
<button :class="['menu-item', { active: current === 'audit' }]" data-testid="admin-menu-audit" @click="switchMenu('audit')">📋 审计日志</button>
|
||||
@@ -311,6 +381,31 @@ onMounted(async () => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="current === 'clients'">
|
||||
<article class="card">
|
||||
<div class="card-title">
|
||||
<span>服务密钥调用方</span>
|
||||
<el-button type="primary" data-testid="admin-btn-new-client" @click="openClient()">+ 创建服务密钥</el-button>
|
||||
</div>
|
||||
<el-alert title="密钥明文只在创建或轮换成功后展示一次;已通过环境变量配置的存量密钥继续有效,但不在本页展示。" type="info" :closable="false" show-icon />
|
||||
<el-table :data="openClients" class="client-table" data-testid="admin-table-clients" empty-text="暂无服务密钥">
|
||||
<el-table-column prop="name" label="调用方" min-width="140" />
|
||||
<el-table-column label="密钥前缀" min-width="145"><template #default="{ row }"><code>{{ row.keyPrefix }}…</code></template></el-table-column>
|
||||
<el-table-column prop="memo" label="用途备注" min-width="180" />
|
||||
<el-table-column label="允许作用域" min-width="200"><template #default="{ row }"><el-tag v-for="(name, index) in row.scopeNames" :key="row.scopeIds[index]" size="small" effect="plain">{{ name }}({{ row.scopeCodes[index] }})</el-tag></template></el-table-column>
|
||||
<el-table-column label="状态" width="90"><template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">{{ row.status === 1 ? '正常' : '已吊销' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="最后使用" min-width="155"><template #default="{ row }">{{ date(row.lastUsedTime) }}</template></el-table-column>
|
||||
<el-table-column label="更新时间" min-width="155"><template #default="{ row }">{{ date(row.modifyTime) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="205" fixed="right"><template #default="{ row }">
|
||||
<el-button link type="primary" :data-testid="`admin-client-edit-${row.id}`" @click="openClient(row)">编辑</el-button>
|
||||
<el-button link type="warning" :data-testid="`admin-client-rotate-${row.id}`" @click="rotateClient(row)">轮换</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" :data-testid="`admin-client-status-${row.id}`" @click="setClientStatus(row)">{{ row.status === 1 ? '吊销' : '启用' }}</el-button>
|
||||
</template></el-table-column>
|
||||
</el-table>
|
||||
<p class="hint">服务密钥用于调用 <code>/api/open/token/ensure</code> 换取业务 Token;作用域决定该密钥能为哪些数据域签发或获取 Token。</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template v-else-if="current === 'tools'">
|
||||
<article class="card">
|
||||
<div class="toolbar">
|
||||
@@ -416,6 +511,25 @@ onMounted(async () => {
|
||||
<template #footer><el-button data-testid="admin-scope-cancel" @click="scopeDialog = false">取消</el-button><el-button type="primary" data-testid="admin-scope-save" @click="saveScope">保存</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="clientDialog" :title="clientForm.id ? '编辑服务密钥' : '创建服务密钥'" width="560px" destroy-on-close>
|
||||
<el-form label-width="92px">
|
||||
<el-form-item label="调用方名称"><el-input v-model="clientForm.name" maxlength="64" placeholder="如 crm" aria-label="服务密钥调用方名称" data-testid="admin-client-name" /></el-form-item>
|
||||
<el-form-item label="用途备注"><el-input v-model="clientForm.memo" type="textarea" maxlength="255" placeholder="如:CRM 合同增量同步" aria-label="服务密钥用途备注" data-testid="admin-client-memo" /></el-form-item>
|
||||
<el-form-item label="允许作用域">
|
||||
<el-checkbox-group v-model="clientForm.scopeIds" data-testid="admin-client-scopes">
|
||||
<el-checkbox
|
||||
v-for="scope in scopes"
|
||||
:key="scope.id"
|
||||
:value="scope.id"
|
||||
:disabled="scope.status !== 1 && !clientForm.scopeIds.includes(scope.id)"
|
||||
>{{ scope.name }}({{ scope.code }})<template v-if="scope.status !== 1"> · 已停用</template></el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<div class="hint" style="margin:4px 0 0">CRM 后台增量同步请选择 sync;其他调用方按实际用途授权。</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button data-testid="admin-client-cancel" @click="clientDialog = false">取消</el-button><el-button type="primary" data-testid="admin-client-save" @click="saveClient">保存</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="scopeToolsDialog" :title="`配置工具 · ${activeScope?.name || ''}`" width="680px" destroy-on-close>
|
||||
<el-checkbox-group v-model="selectedToolIds" class="tool-groups" data-testid="admin-scope-tool-options">
|
||||
<section v-for="(items, category) in groupedTools" :key="category" class="tool-group"><h3>{{ category }}</h3><el-checkbox v-for="tool in items" :key="tool.id" :value="tool.id" :data-testid="`admin-scope-tool-${tool.id}`"><code>{{ tool.name }}</code><small>{{ tool.description }}</small></el-checkbox></section>
|
||||
@@ -432,4 +546,18 @@ onMounted(async () => {
|
||||
</div>
|
||||
<template #footer><el-button type="primary" data-testid="admin-copy-all-tokens" @click="copyAllTokens">全部复制</el-button><el-button data-testid="admin-close-token" @click="issuedTokens = []">我已保存</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :model-value="!!issuedOpenKey" title="服务密钥已生成" width="720px" :close-on-click-modal="false" @close="clearIssuedOpenKey">
|
||||
<el-alert title="明文只展示这一次,请立即复制并交给对应系统安全保存。关闭后后台无法找回,只能轮换。" type="warning" :closable="false" show-icon />
|
||||
<template v-if="issuedOpenKey">
|
||||
<div class="issued-row">
|
||||
<code class="token-secret" data-testid="admin-issued-client-key">{{ issuedOpenKey.plaintext }}</code>
|
||||
<el-button link type="primary" data-testid="admin-copy-client-key" @click="copyText(issuedOpenKey.plaintext)">复制密钥</el-button>
|
||||
</div>
|
||||
<p class="field-label key-example-label">CRM 换取服务 Token 示例</p>
|
||||
<pre class="key-example" data-testid="admin-client-curl">{{ issuedKeyCurl }}</pre>
|
||||
<p class="hint">如果绑定的不是 sync 作用域,请按实际作用域修改请求中的 scopeCode 和主体参数。</p>
|
||||
</template>
|
||||
<template #footer><el-button type="primary" data-testid="admin-copy-client-curl" @click="copyText(issuedKeyCurl)">复制调用示例</el-button><el-button data-testid="admin-close-client-key" @click="clearIssuedOpenKey">我已保存</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
@@ -62,4 +62,7 @@ code { font-family: Menlo, Consolas, monospace; font-size: 12px; }
|
||||
.token-secret { display: block; margin-top: 14px; padding: 14px; word-break: break-all; border-radius: 4px; background: #f4f4f5; font-size: 13px; }
|
||||
.issued-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
|
||||
.issued-row .token-secret { flex: 1; margin-top: 0; padding: 9px 12px; }
|
||||
.client-table { margin-top: 16px; }
|
||||
.key-example-label { margin: 18px 0 7px; }
|
||||
.key-example { margin: 0; padding: 14px; overflow: auto; color: #e6edf3; background: #1f2328; border-radius: 5px; font: 12px/1.6 Menlo, Consolas, monospace; white-space: pre-wrap; word-break: break-all; }
|
||||
@media (max-width: 1200px) { .token-layout { grid-template-columns: 300px minmax(640px, 1fr); } }
|
||||
|
||||
@@ -9,11 +9,15 @@ test('prototype interactions expose stable test ids', async () => {
|
||||
'admin-input-password',
|
||||
'admin-btn-login',
|
||||
'admin-menu-tokens',
|
||||
'admin-menu-clients',
|
||||
'admin-menu-tools',
|
||||
'admin-menu-scopes',
|
||||
'admin-menu-audit',
|
||||
'admin-input-user-search',
|
||||
'admin-btn-issue-token',
|
||||
'admin-btn-new-client',
|
||||
'admin-table-clients',
|
||||
'admin-issued-client-key',
|
||||
'admin-table-tools',
|
||||
'admin-btn-new-scope',
|
||||
'admin-table-scopes',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 回滚 V4:删除 principal_type / subject_name 列,user_id 恢复 NOT NULL。
|
||||
-- 回滚后服务 Token 无法被识别(AuthService 不再区分主体类型),存量 principal_type='service'
|
||||
-- 的 Token 会因 user_id 为空无法解析人员而 401,等于全部失效;
|
||||
-- 回滚前应先禁用或删除服务 Token(UPDATE agent_api_key SET status=0 WHERE principal_type='service')。
|
||||
-- 残余 user_id 为 NULL 的行回填 0 以通过 NOT NULL 约束(这些行已不再参与鉴权)。
|
||||
UPDATE agent_api_key SET user_id = 0 WHERE user_id IS NULL;
|
||||
|
||||
ALTER TABLE agent_api_key
|
||||
DROP COLUMN principal_type;
|
||||
|
||||
ALTER TABLE agent_api_key
|
||||
DROP COLUMN subject_name;
|
||||
|
||||
ALTER TABLE agent_api_key
|
||||
MODIFY COLUMN user_id BIGINT NOT NULL COMMENT '所属人(m0.user.id)';
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 回滚 V5__tool_description_length.sql
|
||||
-- 先截断超长描述再收窄列,避免 Data truncation
|
||||
UPDATE tool SET description = LEFT(description, 512) WHERE CHAR_LENGTH(description) > 512;
|
||||
|
||||
ALTER TABLE tool
|
||||
MODIFY COLUMN description VARCHAR(512) NULL COMMENT '工具描述';
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS app_open_client_scope;
|
||||
DROP TABLE IF EXISTS app_open_client;
|
||||
@@ -14,10 +14,36 @@ INSERT INTO agent_scope (name, code, memo, status, create_id)
|
||||
SELECT '运营', 'operation', 'dev 种子:运营作用域', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM agent_scope WHERE code = 'operation');
|
||||
|
||||
-- 作用域-工具:sys.ping / agent.count / agent.list.query / property.list.query
|
||||
-- 作用域-工具:sys.ping / agent.count / agent.list.query / property.list.query / agent.detail.query / property.detail.query
|
||||
-- / mall.order.list.query / mall.order.detail.query / mall.order.logistics.query
|
||||
-- / contract.list.query / contract.detail.query / integration.context.get
|
||||
INSERT INTO scope_tool (scope_id, tool_id, create_id)
|
||||
SELECT s.id, t.id, 0 FROM agent_scope s JOIN tool t
|
||||
WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query')
|
||||
WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query', 'agent.detail.query', 'property.detail.query', 'mall.order.list.query', 'mall.order.detail.query', 'mall.order.logistics.query', 'contract.list.query', 'contract.detail.query', 'integration.context.get')
|
||||
AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id);
|
||||
|
||||
-- 作用域:hardware(出纸机查询)
|
||||
INSERT INTO agent_scope (name, code, memo, status, create_id)
|
||||
SELECT '出纸机', 'hardware', 'dev 种子:出纸机查询作用域', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM agent_scope WHERE code = 'hardware');
|
||||
|
||||
-- 作用域-工具:hardware.list.query / hardware.detail.query
|
||||
INSERT INTO scope_tool (scope_id, tool_id, create_id)
|
||||
SELECT s.id, t.id, 0 FROM agent_scope s JOIN tool t
|
||||
WHERE s.code = 'hardware' AND t.name IN ('hardware.list.query', 'hardware.detail.query')
|
||||
AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id);
|
||||
|
||||
-- 作用域:sync(SYNC 增量同步;changes 工具由 REQ-20260911-0013 注册并绑定到本作用域)
|
||||
-- 个人 Token 禁调 SYNC 工具靠 token_scope 白名单天然生效:个人 Token 不绑定本作用域即 PERMISSION_DENIED,
|
||||
-- 无需任何拦截代码。只为服务 Token(principal_type=service)勾选 sync 作用域。
|
||||
INSERT INTO agent_scope (name, code, memo, status, create_id)
|
||||
SELECT '数据同步', 'sync', 'SYNC 增量同步作用域(5628 注册 *.changes.list 工具后绑定,仅服务 Token 使用)', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM agent_scope WHERE code = 'sync');
|
||||
|
||||
-- sync 作用域-工具:服务 Token 自检、三条变化流及发送前合同复核
|
||||
INSERT INTO scope_tool (scope_id, tool_id, create_id)
|
||||
SELECT s.id, t.id, 0 FROM agent_scope s JOIN tool t
|
||||
WHERE s.code = 'sync' AND t.name IN ('integration.context.get', 'users.changes.list', 'properties.changes.list', 'contracts.changes.list', 'contracts.current.batch_get')
|
||||
AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -156,6 +156,12 @@ public class AdminConsoleController {
|
||||
tokenService.delete(id, adminId(session));
|
||||
}
|
||||
|
||||
/** 轮换:禁用旧把并按原主体/作用域签新把,明文仅在本次响应返回(REQ-20260911-0014)。 */
|
||||
@PostMapping("/tokens/{id}/rotate")
|
||||
public List<AdminTokenService.IssuedToken> rotateToken(@PathVariable Long id, HttpSession session) {
|
||||
return tokenService.rotate(id, adminId(session));
|
||||
}
|
||||
|
||||
@GetMapping("/tools")
|
||||
public List<ToolView> tools(@RequestParam(defaultValue = "") String category,
|
||||
@RequestParam(defaultValue = "") String kind,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package me.iiv.agent.admin;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import me.iiv.agent.open.OpenClientService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 复用现有管理员会话的服务密钥管理接口。 */
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/open-clients")
|
||||
public class AdminOpenClientController {
|
||||
|
||||
private final OpenClientService service;
|
||||
|
||||
public AdminOpenClientController(OpenClientService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<OpenClientService.ClientView> list() {
|
||||
return service.list();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public OpenClientService.IssuedClient create(@RequestBody OpenClientService.ClientRequest body,
|
||||
HttpSession session) {
|
||||
return service.create(body, adminId(session));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void update(@PathVariable Long id, @RequestBody OpenClientService.ClientRequest body,
|
||||
HttpSession session) {
|
||||
service.update(id, body, adminId(session));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/status")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void status(@PathVariable Long id, @RequestBody StatusRequest body, HttpSession session) {
|
||||
if (body == null) throw new org.springframework.web.server.ResponseStatusException(
|
||||
HttpStatus.BAD_REQUEST, "请求内容不能为空");
|
||||
service.status(id, body.status(), adminId(session));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/rotate")
|
||||
public OpenClientService.IssuedClient rotate(@PathVariable Long id, HttpSession session) {
|
||||
return service.rotate(id, adminId(session));
|
||||
}
|
||||
|
||||
private static Long adminId(HttpSession session) {
|
||||
return (Long) session.getAttribute(AdminSessionFilter.ADMIN_SESSION_ID);
|
||||
}
|
||||
|
||||
public record StatusRequest(byte status) {}
|
||||
}
|
||||
@@ -55,23 +55,66 @@ public class AdminTokenService {
|
||||
|
||||
/** 签发一把绑定单个作用域的 Token;人员/作用域合法性由调用方保证。 */
|
||||
public IssuedToken issueSingle(Long userId, String memo, Long scopeId, Date expireTime, Long creatorId) {
|
||||
String plaintext = "xqak_" + AdminAuthController.randomHex(24);
|
||||
AgentApiKey token = new AgentApiKey();
|
||||
AgentApiKey token = newToken(memo, expireTime, creatorId);
|
||||
token.setUserId(userId);
|
||||
token.setKeyPrefix(plaintext.substring(0, 13));
|
||||
token.setKeyHash(AuthService.sha256Hex(plaintext));
|
||||
token.setKeyCipher(tokenCipher.encrypt(plaintext));
|
||||
token.setPrincipalType(AgentApiKey.PRINCIPAL_TYPE_USER);
|
||||
return insert(token, scopeId, creatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 签发服务 Token(principal_type=service):不绑定人员,行级权限不适用,
|
||||
* 数据范围由绑定的作用域决定(scope 粒度)。subjectName 标识服务主体,供 get-or-create 查找。
|
||||
*/
|
||||
public IssuedToken issueServiceToken(String serviceName, String memo, Long scopeId, Date expireTime, Long creatorId) {
|
||||
if (serviceName == null || !serviceName.trim().matches("[a-z][a-z0-9_-]{0,63}")) bad("服务主体标识格式无效");
|
||||
AgentApiKey token = newToken(memo, expireTime, creatorId);
|
||||
token.setPrincipalType(AgentApiKey.PRINCIPAL_TYPE_SERVICE);
|
||||
token.setSubjectName(serviceName.trim());
|
||||
return insert(token, scopeId, creatorId);
|
||||
}
|
||||
|
||||
private AgentApiKey newToken(String memo, Date expireTime, Long creatorId) {
|
||||
AgentApiKey token = new AgentApiKey();
|
||||
token.setMemo(trim(memo, 255));
|
||||
token.setStatus((byte) 1);
|
||||
token.setExpireTime(expireTime);
|
||||
token.setCreateId(creatorId);
|
||||
token.setModifyId(creatorId);
|
||||
token.setSoftDelete((byte) 0);
|
||||
return token;
|
||||
}
|
||||
|
||||
private IssuedToken insert(AgentApiKey token, Long scopeId, Long creatorId) {
|
||||
String plaintext = "xqak_" + AdminAuthController.randomHex(24);
|
||||
token.setKeyPrefix(plaintext.substring(0, 13));
|
||||
token.setKeyHash(AuthService.sha256Hex(plaintext));
|
||||
token.setKeyCipher(tokenCipher.encrypt(plaintext));
|
||||
tokenMapper.insert(token);
|
||||
replaceScopes(token.getId(), List.of(scopeId), creatorId);
|
||||
return new IssuedToken(plaintext, view(token));
|
||||
}
|
||||
|
||||
/** 轮换:禁用旧把(吊销立即生效,AkAuthFilter 校验 status),按原主体/作用域/过期时间签新把,明文逐把返回。 */
|
||||
@Transactional(transactionManager = "svcTransactionManager")
|
||||
public List<IssuedToken> rotate(Long tokenId, Long operatorId) {
|
||||
AgentApiKey current = activeToken(tokenId);
|
||||
List<Long> scopeIds = relationMapper.selectList(new LambdaQueryWrapper<TokenScope>()
|
||||
.eq(TokenScope::getTokenId, tokenId).eq(TokenScope::getSoftDelete, 0))
|
||||
.stream().map(TokenScope::getScopeId).toList();
|
||||
if (scopeIds.isEmpty()) bad("Token 未绑定作用域,无法轮换");
|
||||
current.setStatus((byte) 0);
|
||||
current.setModifyId(operatorId);
|
||||
tokenMapper.updateById(current);
|
||||
boolean service = AgentApiKey.PRINCIPAL_TYPE_SERVICE.equals(current.getPrincipalType());
|
||||
return scopeIds.stream()
|
||||
.map(scopeId -> service
|
||||
? issueServiceToken(current.getSubjectName(), current.getMemo(), scopeId,
|
||||
current.getExpireTime(), operatorId)
|
||||
: issueSingle(current.getUserId(), current.getMemo(), scopeId,
|
||||
current.getExpireTime(), operatorId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 查找某人员在某作用域下的有效 Token(正常状态且未过期),取最新一把;无则 null。 */
|
||||
public AgentApiKey findActiveToken(Long userId, Long scopeId) {
|
||||
List<Long> tokenIds = relationMapper.selectList(new LambdaQueryWrapper<TokenScope>()
|
||||
@@ -90,6 +133,25 @@ public class AdminTokenService {
|
||||
.stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
/** 查找某服务主体在某作用域下的有效 Token(正常状态且未过期),取最新一把;无则 null。 */
|
||||
public AgentApiKey findActiveServiceToken(String serviceName, Long scopeId) {
|
||||
List<Long> tokenIds = relationMapper.selectList(new LambdaQueryWrapper<TokenScope>()
|
||||
.eq(TokenScope::getScopeId, scopeId).eq(TokenScope::getSoftDelete, 0))
|
||||
.stream().map(TokenScope::getTokenId).toList();
|
||||
if (tokenIds.isEmpty()) return null;
|
||||
return tokenMapper.selectList(new LambdaQueryWrapper<AgentApiKey>()
|
||||
.in(AgentApiKey::getId, tokenIds)
|
||||
.eq(AgentApiKey::getPrincipalType, AgentApiKey.PRINCIPAL_TYPE_SERVICE)
|
||||
.eq(AgentApiKey::getSubjectName, serviceName)
|
||||
.eq(AgentApiKey::getStatus, 1)
|
||||
.eq(AgentApiKey::getSoftDelete, 0)
|
||||
.and(w -> w.isNull(AgentApiKey::getExpireTime)
|
||||
.or().gt(AgentApiKey::getExpireTime, new Date()))
|
||||
.orderByDesc(AgentApiKey::getCreateTime)
|
||||
.last("LIMIT 1"))
|
||||
.stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
@Transactional(transactionManager = "svcTransactionManager")
|
||||
public void update(Long tokenId, String memo, List<Long> scopeIds, Date expireTime, Long adminId) {
|
||||
AgentApiKey current = activeToken(tokenId);
|
||||
@@ -133,6 +195,7 @@ public class AdminTokenService {
|
||||
new LambdaQueryWrapper<AgentScope>().in(AgentScope::getId, scopeIds))
|
||||
.stream().map(AgentScope::getName).toList();
|
||||
return new TokenView(token.getId(), token.getKeyPrefix(), token.getMemo(), token.getStatus(),
|
||||
token.getPrincipalType(), token.getSubjectName(),
|
||||
token.getExpireTime(), token.getLastUsedTime(), scopeIds, scopeNames);
|
||||
}
|
||||
|
||||
@@ -187,7 +250,8 @@ public class AdminTokenService {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
public record TokenView(Long id, String keyPrefix, String memo, Byte status, Date expireTime,
|
||||
public record TokenView(Long id, String keyPrefix, String memo, Byte status,
|
||||
String principalType, String subjectName, Date expireTime,
|
||||
Date lastUsedTime, List<Long> scopeIds, List<String> scopeNames) {}
|
||||
public record IssuedToken(String plaintext, TokenView token) {}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
/**
|
||||
* 工具调用审计(设计文档 §3.2 第 5 步)。不记录 ak。
|
||||
* 工具调用审计(设计文档 §3.2 第 5 步)。不记录 ak;
|
||||
* 入参摘要与结果信息中的手机号一律脱敏(REQ-20260911-0011)。
|
||||
*/
|
||||
@Component
|
||||
public class AuditService {
|
||||
@@ -33,9 +34,9 @@ public class AuditService {
|
||||
auditLog.setUserId(ctx == null ? null : ctx.getUserId());
|
||||
auditLog.setTokenId(ctx == null ? null : ctx.getTokenId());
|
||||
auditLog.setToolName(toolName);
|
||||
auditLog.setParamsDigest(truncate(paramsJson, PARAMS_DIGEST_MAX));
|
||||
auditLog.setParamsDigest(truncate(MaskingUtil.maskPhones(paramsJson), PARAMS_DIGEST_MAX));
|
||||
auditLog.setResultStatus(resultStatus);
|
||||
auditLog.setResultMessage(truncate(resultMessage, PARAMS_DIGEST_MAX));
|
||||
auditLog.setResultMessage(truncate(MaskingUtil.maskPhones(resultMessage), PARAMS_DIGEST_MAX));
|
||||
auditLog.setCostMs(costMs);
|
||||
auditLog.setClientIp(clientIp());
|
||||
auditLog.setClientType(clientType());
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package me.iiv.agent.common;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 日志/审计脱敏(REQ-20260911-0011):手机号明文脱敏为 138****5678 形式。
|
||||
* Token/ak 不落日志由调用方保证(现有日志点均不输出 ak),本工具只负责手机号。
|
||||
*/
|
||||
public final class MaskingUtil {
|
||||
|
||||
/** 中国大陆手机号:1 开头 11 位,第二位 3-9 */
|
||||
private static final Pattern PHONE = Pattern.compile("1[3-9]\\d{9}");
|
||||
|
||||
private MaskingUtil() {
|
||||
}
|
||||
|
||||
public static String maskPhones(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
Matcher matcher = PHONE.matcher(text);
|
||||
StringBuilder sb = new StringBuilder(text.length());
|
||||
while (matcher.find()) {
|
||||
String phone = matcher.group();
|
||||
matcher.appendReplacement(sb,
|
||||
Matcher.quoteReplacement(phone.substring(0, 3) + "****" + phone.substring(7)));
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,15 @@ public class M0DataSourceConfig {
|
||||
|
||||
@Primary
|
||||
@Bean(name = "m0SqlSessionFactory")
|
||||
public SqlSessionFactory m0SqlSessionFactory(@Qualifier("m0DataSource") DataSource dataSource) throws Exception {
|
||||
public SqlSessionFactory m0SqlSessionFactory(@Qualifier("m0DataSource") DataSource dataSource,
|
||||
SyncQueryTimeoutInterceptor syncQueryTimeoutInterceptor)
|
||||
throws Exception {
|
||||
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
|
||||
bean.setDataSource(dataSource);
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
bean.setConfiguration(configuration);
|
||||
bean.setPlugins(syncQueryTimeoutInterceptor);
|
||||
return bean.getObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,15 @@ public class M1DataSourceConfig {
|
||||
}
|
||||
|
||||
@Bean(name = "m1SqlSessionFactory")
|
||||
public SqlSessionFactory m1SqlSessionFactory(@Qualifier("m1DataSource") DataSource dataSource) throws Exception {
|
||||
public SqlSessionFactory m1SqlSessionFactory(@Qualifier("m1DataSource") DataSource dataSource,
|
||||
SyncQueryTimeoutInterceptor syncQueryTimeoutInterceptor)
|
||||
throws Exception {
|
||||
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
|
||||
bean.setDataSource(dataSource);
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
bean.setConfiguration(configuration);
|
||||
bean.setPlugins(syncQueryTimeoutInterceptor);
|
||||
return bean.getObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,15 @@ public class M2DataSourceConfig {
|
||||
}
|
||||
|
||||
@Bean(name = "m2SqlSessionFactory")
|
||||
public SqlSessionFactory m2SqlSessionFactory(@Qualifier("m2DataSource") DataSource dataSource) throws Exception {
|
||||
public SqlSessionFactory m2SqlSessionFactory(@Qualifier("m2DataSource") DataSource dataSource,
|
||||
SyncQueryTimeoutInterceptor syncQueryTimeoutInterceptor)
|
||||
throws Exception {
|
||||
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
|
||||
bean.setDataSource(dataSource);
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
bean.setConfiguration(configuration);
|
||||
bean.setPlugins(syncQueryTimeoutInterceptor);
|
||||
return bean.getObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package me.iiv.agent.config;
|
||||
|
||||
import me.iiv.agent.common.AuditService;
|
||||
import me.iiv.agent.mcp.GuardedToolCallbackProvider;
|
||||
import me.iiv.agent.mcp.RateLimitService;
|
||||
import me.iiv.agent.mcp.ToolObject;
|
||||
import me.iiv.agent.security.ToolGuard;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
@@ -20,10 +21,11 @@ import java.util.List;
|
||||
public class McpToolConfig {
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider xiaoquTools(List<ToolObject> toolObjects, ToolGuard toolGuard, AuditService auditService) {
|
||||
public ToolCallbackProvider xiaoquTools(List<ToolObject> toolObjects, ToolGuard toolGuard,
|
||||
AuditService auditService, RateLimitService rateLimitService) {
|
||||
ToolCallbackProvider methodProvider = MethodToolCallbackProvider.builder()
|
||||
.toolObjects(toolObjects.toArray())
|
||||
.build();
|
||||
return new GuardedToolCallbackProvider(methodProvider, toolGuard, auditService);
|
||||
return new GuardedToolCallbackProvider(methodProvider, toolGuard, auditService, rateLimitService);
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package me.iiv.agent.config;
|
||||
|
||||
import me.iiv.agent.mcp.sync.SyncQueryTimeoutHolder;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.apache.ibatis.plugin.Intercepts;
|
||||
import org.apache.ibatis.plugin.Invocation;
|
||||
import org.apache.ibatis.plugin.Signature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
|
||||
/**
|
||||
* 慢查询超时(REQ-20260911-0013 资源隔离):SyncQueryTimeoutHolder 非空时,
|
||||
* 给本线程 prepare 出的 Statement 套 queryTimeout(秒),
|
||||
* 防止 SYNC 工具的后台全量扫描拖慢销售在线点查。未设置时完全不影响现有查询。
|
||||
*/
|
||||
@Component
|
||||
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare",
|
||||
args = {Connection.class, Integer.class}))
|
||||
public class SyncQueryTimeoutInterceptor implements Interceptor {
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
Object statement = invocation.proceed();
|
||||
Integer timeout = SyncQueryTimeoutHolder.current();
|
||||
if (timeout != null && timeout > 0 && statement instanceof Statement stmt) {
|
||||
stmt.setQueryTimeout(timeout);
|
||||
}
|
||||
return statement;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -62,21 +62,11 @@ public class Agent implements Serializable {
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 渠道编号(Z:总部;F:分公司;D:代理)
|
||||
*/
|
||||
private String channelNo;
|
||||
|
||||
/**
|
||||
* 上级id
|
||||
*/
|
||||
private Long supervisorId;
|
||||
|
||||
/**
|
||||
* 上级代理
|
||||
*/
|
||||
private String supervisorName;
|
||||
|
||||
/**
|
||||
* 企业地址
|
||||
*/
|
||||
@@ -137,6 +127,11 @@ public class Agent implements Serializable {
|
||||
*/
|
||||
private Integer level;
|
||||
|
||||
/**
|
||||
* 渠道编号(Z:总部;F:分公司;D:代理)
|
||||
*/
|
||||
private String channelNo;
|
||||
|
||||
/**
|
||||
* 户名
|
||||
*/
|
||||
@@ -192,6 +187,36 @@ public class Agent implements Serializable {
|
||||
*/
|
||||
private String shortName;
|
||||
|
||||
/**
|
||||
* 金蝶供应商编号
|
||||
*/
|
||||
private String supplierNo;
|
||||
|
||||
/**
|
||||
* 开票通讯地址
|
||||
*/
|
||||
private String invoiceAddress;
|
||||
|
||||
/**
|
||||
* 开票联系电话
|
||||
*/
|
||||
private String invoiceContact;
|
||||
|
||||
/**
|
||||
* 组织内码
|
||||
*/
|
||||
private String saleOrgInternalId;
|
||||
|
||||
/**
|
||||
* 客户分组
|
||||
*/
|
||||
private String custerGroupNo;
|
||||
|
||||
/**
|
||||
* 金蝶内码
|
||||
*/
|
||||
private Long jindieInternalId;
|
||||
|
||||
/**
|
||||
* 金蝶客户编号
|
||||
*/
|
||||
@@ -212,41 +237,16 @@ public class Agent implements Serializable {
|
||||
*/
|
||||
private String saleOrgId;
|
||||
|
||||
/**
|
||||
* 金蝶内码
|
||||
*/
|
||||
private Long jindieInternalId;
|
||||
|
||||
/**
|
||||
* 组织内码
|
||||
*/
|
||||
private String saleOrgInternalId;
|
||||
|
||||
/**
|
||||
* 客户分组
|
||||
*/
|
||||
private String custerGroupNo;
|
||||
|
||||
/**
|
||||
* 金蝶供应商编号
|
||||
*/
|
||||
private String supplierNo;
|
||||
|
||||
/**
|
||||
* 开票通讯地址
|
||||
*/
|
||||
private String invoiceAddress;
|
||||
|
||||
/**
|
||||
* 开票联系电话
|
||||
*/
|
||||
private String invoiceContact;
|
||||
|
||||
/**
|
||||
* 金蝶客户名称
|
||||
*/
|
||||
private String jinideName;
|
||||
|
||||
/**
|
||||
* 上级代理
|
||||
*/
|
||||
private String supervisorName;
|
||||
|
||||
/**
|
||||
* 最后开票时间
|
||||
*/
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package me.iiv.agent.entity.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("district_district_strategy")
|
||||
public class DistrictDistrictStrategy implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private String districtName;
|
||||
private String address;
|
||||
private Long propertyId;
|
||||
private Long agentId;
|
||||
private String province;
|
||||
private String city;
|
||||
private String countyseat;
|
||||
private String memo;
|
||||
private String contact;
|
||||
private String mail;
|
||||
@TableField("buildCount")
|
||||
private Integer buildCount;
|
||||
private String districtNumber;
|
||||
private String phone;
|
||||
private Integer installType;
|
||||
private Integer warning;
|
||||
private Integer airdetectorFlag;
|
||||
private Integer warning2;
|
||||
private Integer warning3;
|
||||
private Integer warningTime;
|
||||
private Integer onlineState;
|
||||
private Integer paperPercentWarn;
|
||||
private Integer pointState;
|
||||
private Integer assignmentWorkState;
|
||||
private Integer airState;
|
||||
private String businessFormat;
|
||||
private String posDevicePaperNorm;
|
||||
private String toiletDevicePaperNorm;
|
||||
private Integer messageFlag;
|
||||
private Integer reportFlag;
|
||||
private Double fineMoney;
|
||||
private Integer lockMachineFlag;
|
||||
private Integer eventRepairFlag;
|
||||
private Integer rfidUseFlag;
|
||||
private Integer calibratedFlag;
|
||||
private String salesName;
|
||||
private Long salesId;
|
||||
private Integer waitTimes;
|
||||
private Integer noPaperState;
|
||||
private Integer lowerPaperState;
|
||||
private Integer districtState;
|
||||
private Integer deviceSalesType;
|
||||
private Integer paperSalesType;
|
||||
private LocalDate weaningDate;
|
||||
private LocalDate honourDate;
|
||||
private LocalDate testDate;
|
||||
private Integer powerWarning;
|
||||
private Integer pointDeduction;
|
||||
private Integer testDays;
|
||||
private Integer illegalPaperDisable;
|
||||
private Integer effectiveUseTime;
|
||||
private Long attributeId;
|
||||
private String attributeName;
|
||||
private Integer hardwareCount;
|
||||
private Double consumablesPrice;
|
||||
private Integer billDate;
|
||||
private Integer foamCountWarning;
|
||||
private Integer foamPowerWarning;
|
||||
private Long serviceCompanyId;
|
||||
private String serviceCompany;
|
||||
private String billType;
|
||||
private Integer consumablesType;
|
||||
private Integer maintainOrderWarning;
|
||||
private Integer lnfluenceLevel;
|
||||
private Integer isSmart;
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package me.iiv.agent.entity.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* hardware_view 视图(m0 库),字段 1:1 照搬老项目
|
||||
* xiaoqu-publicbeanandmap beans/b0/HardwareView.java。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("hardware_view")
|
||||
public class HardwareView implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 创建id */
|
||||
private Long createId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 修改id */
|
||||
private Long modifyId;
|
||||
|
||||
/** 修改时间 */
|
||||
private Date modifyTime;
|
||||
|
||||
/** 数据状态 */
|
||||
private Integer softDelete;
|
||||
|
||||
/** 设备编号 */
|
||||
private String no;
|
||||
|
||||
/** 位置信息 */
|
||||
private String address;
|
||||
|
||||
/** 位置id */
|
||||
private Long positionId;
|
||||
|
||||
/** 位置信息 */
|
||||
private String position;
|
||||
|
||||
/** sim卡号 */
|
||||
private String simNo;
|
||||
|
||||
/** sn码 */
|
||||
private String snCode;
|
||||
|
||||
/** 状态1:在线;2:故障;3:离线 */
|
||||
private Integer status;
|
||||
|
||||
/** 设备随机码 */
|
||||
private String checkCode;
|
||||
|
||||
/** 固件版本 */
|
||||
private String firmwareVersion;
|
||||
|
||||
/** 厕所id */
|
||||
private Long toiletId;
|
||||
|
||||
/** 楼id */
|
||||
private Long buildingId;
|
||||
|
||||
/** 楼层 */
|
||||
private String floor;
|
||||
|
||||
/** 区域id */
|
||||
private Long districtId;
|
||||
|
||||
/** 物业id */
|
||||
private Long propertyId;
|
||||
|
||||
/** 代理id */
|
||||
private Long agentId;
|
||||
|
||||
/** 设备型号 */
|
||||
private String version;
|
||||
|
||||
/** 备注 */
|
||||
private String memo;
|
||||
|
||||
/** 保洁id */
|
||||
private Long userId;
|
||||
|
||||
/** 电力支持 */
|
||||
private Integer powerSupply;
|
||||
|
||||
/** 感应抽纸长度 */
|
||||
private Integer senseLength;
|
||||
|
||||
/** 扫码抽纸长度 */
|
||||
private Integer scanLength;
|
||||
|
||||
/** 出纸模式 */
|
||||
private Integer paperType;
|
||||
|
||||
/** 纸规格 */
|
||||
private String paperNorms;
|
||||
|
||||
/** 感应出纸次数 */
|
||||
private Integer senseCount;
|
||||
|
||||
/** 扫码出纸次数 */
|
||||
private Integer scanCount;
|
||||
|
||||
/** 剩余次数 */
|
||||
private Integer paperSurplus;
|
||||
|
||||
/** 出纸计数 */
|
||||
private Long paperCount;
|
||||
|
||||
/** sn码 */
|
||||
private String temSnCode;
|
||||
|
||||
/** rfid */
|
||||
private String rfid;
|
||||
|
||||
/** 硬件名称 */
|
||||
private String name;
|
||||
|
||||
/** 安装场景0:厕位内:1:厕位外:2其它 */
|
||||
private Integer installScenes;
|
||||
|
||||
/** 安装方式0:免打孔 1:打孔 */
|
||||
private Integer installMode;
|
||||
|
||||
/** 供电方式 0:电源 1:电池 */
|
||||
private Integer poweredType;
|
||||
|
||||
/** 设备上下线状态 */
|
||||
private Integer online;
|
||||
|
||||
/** 蓝牙名称 */
|
||||
private String bluetoothName;
|
||||
|
||||
/** imsi码 */
|
||||
private String imsi;
|
||||
|
||||
/** 蓝牙地址 */
|
||||
private String bluetoothMac;
|
||||
|
||||
/** 上线时间 */
|
||||
private Date onlineTime;
|
||||
|
||||
/** 芯片号 */
|
||||
private String chip;
|
||||
|
||||
/** 主板号 */
|
||||
private String motherboard;
|
||||
|
||||
private String sysversion;
|
||||
|
||||
/** 感应出纸次数 */
|
||||
private Integer paperState;
|
||||
|
||||
/** 扫码出纸次数 */
|
||||
private Integer powerState;
|
||||
|
||||
private Integer positionState;
|
||||
|
||||
/** 剩余次数 */
|
||||
private Integer useState;
|
||||
|
||||
/** 质保期 */
|
||||
private Integer warrantyPeriod;
|
||||
|
||||
/** 运行模式 */
|
||||
private Integer runlMode;
|
||||
|
||||
/** 锁状态 */
|
||||
private Integer lockState;
|
||||
|
||||
/** 设备类型 */
|
||||
private Integer deviceType;
|
||||
|
||||
/** 清理用纸卷计数 */
|
||||
private Integer cleanRfidCount;
|
||||
|
||||
/** 工程模式0:关;1:开 */
|
||||
private Integer projectModel;
|
||||
|
||||
/** 出纸模式0:关闭;1:开启 */
|
||||
private Integer paperOutState;
|
||||
|
||||
/** 机芯更换时间 */
|
||||
private Date machineChipTime;
|
||||
|
||||
/** 机芯编号 */
|
||||
private String machineChipCode;
|
||||
|
||||
/** 当前机芯编号 */
|
||||
private String currentChip;
|
||||
|
||||
/** 锁定机器状态0:未锁定;1:已锁定 */
|
||||
private Integer lockMachineState;
|
||||
|
||||
/** 语音版本 */
|
||||
private String voiceVersion;
|
||||
|
||||
/** 校准标识0:否;1:是 */
|
||||
private Integer calibratFlag;
|
||||
|
||||
/** 满卷标识0:否;1:是 */
|
||||
private Integer fullRollFlag;
|
||||
|
||||
/** 中校准锁定开关 */
|
||||
private Integer middleFlag;
|
||||
|
||||
/** 低校准锁定开关 */
|
||||
private Integer lowerFlag;
|
||||
|
||||
/** 纸校准锁定开关 */
|
||||
private Integer paperLockFlag;
|
||||
|
||||
/** 低故障开关(0:故障;1:正常) */
|
||||
private Integer lowerFaultFlag;
|
||||
|
||||
/** 中故障开关(0:故障;1:正常) */
|
||||
private Integer middleFaultFlag;
|
||||
|
||||
/** 当前锁定状态出纸次数 */
|
||||
private Integer paperLockCount;
|
||||
|
||||
/** 保修截止日期 */
|
||||
private Date warrantyExpirationTime;
|
||||
|
||||
/** 首次上线时间 */
|
||||
private Date firstOnTime;
|
||||
|
||||
/** 广告机版本 */
|
||||
private String advertiseVersion;
|
||||
|
||||
/** 非法纸禁出状态(0:禁出;1:未禁出) */
|
||||
private Integer illegalPaperDisableState;
|
||||
|
||||
/** 是否绑定NFC 0:否;1:是 */
|
||||
private Integer nfcFlag;
|
||||
|
||||
/** nfcId */
|
||||
private String nfcId;
|
||||
|
||||
/** 定时语音播放0:关;1:开 */
|
||||
private Integer timerVoiceFlag;
|
||||
|
||||
/** 设定时长(分) */
|
||||
private Integer timerVoiceTiming;
|
||||
|
||||
/** 定时出纸0:关;1:开 */
|
||||
private Integer timerPaperoutFlag;
|
||||
|
||||
/** 设定时长(分) */
|
||||
private Integer timerPaperoutTiming;
|
||||
|
||||
/** 定时出纸长度 */
|
||||
private Integer timerPaperoutLength;
|
||||
|
||||
/** 电池电量异常标记0:否;1:是 */
|
||||
private Integer batteryFaultFlag;
|
||||
|
||||
/** 电池温度 */
|
||||
private Integer batteryTemp;
|
||||
|
||||
/** 网络信号 */
|
||||
private Integer networkSignal;
|
||||
|
||||
/** 充电循环次数 */
|
||||
private Integer batteryCircle;
|
||||
|
||||
/** 生产备注 */
|
||||
private String prdMemo;
|
||||
|
||||
/** 备注 */
|
||||
private String runMemo;
|
||||
|
||||
/** 电流 */
|
||||
private Integer current;
|
||||
|
||||
/** 电压 */
|
||||
private Integer voltage;
|
||||
|
||||
/** 计租标记(0否1是) */
|
||||
private Integer rentFlag;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package me.iiv.agent.entity.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 发票信息表
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("invoice_info")
|
||||
public class InvoiceInfo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createName;
|
||||
|
||||
/**
|
||||
* 修改id
|
||||
*/
|
||||
private Long modifyId;
|
||||
|
||||
/**
|
||||
* 修改人
|
||||
*/
|
||||
private String modifyName;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
/**
|
||||
* 类型(0:代理;1:物业)
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 客户ID
|
||||
*/
|
||||
private Long customerId;
|
||||
|
||||
/**
|
||||
* 客户名称
|
||||
*/
|
||||
private String customerName;
|
||||
|
||||
/**
|
||||
* 开票名称
|
||||
*/
|
||||
private String invoiceName;
|
||||
|
||||
/**
|
||||
* 统一社会信用代码
|
||||
*/
|
||||
private String identificationNumber;
|
||||
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
private String contact;
|
||||
|
||||
/**
|
||||
* 联系人邮箱
|
||||
*/
|
||||
private String mail;
|
||||
|
||||
/**
|
||||
* 银行账户户名称
|
||||
*/
|
||||
private String accountName;
|
||||
|
||||
/**
|
||||
* 银行账号
|
||||
*/
|
||||
private String accountNo;
|
||||
|
||||
/**
|
||||
* 开户行名称
|
||||
*/
|
||||
private String accountBank;
|
||||
|
||||
/**
|
||||
* 开票通讯地址
|
||||
*/
|
||||
private String invoiceAddress;
|
||||
|
||||
/**
|
||||
* 开票联系电话
|
||||
*/
|
||||
private String invoiceContact;
|
||||
|
||||
/**
|
||||
* 备注信息
|
||||
*/
|
||||
private String memo;
|
||||
|
||||
/**
|
||||
* 默认标识(0:非默认;1:默认)
|
||||
*/
|
||||
private Integer defaultFlag;
|
||||
|
||||
/**
|
||||
* 发票抬头类型(0:企业单位;1:个人|非企业)
|
||||
*/
|
||||
private Integer invoiceType;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package me.iiv.agent.entity.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* networkcard 联网卡列表(m0 库),字段照搬老项目
|
||||
* xiaoqu-publicbeanandmap beans/b0/Networkcard.java。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("networkcard")
|
||||
public class Networkcard implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 创建id */
|
||||
private Long createId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 修改id */
|
||||
private Long modifyId;
|
||||
|
||||
/** 修改时间 */
|
||||
private Date modifyTime;
|
||||
|
||||
/** 数据状态 */
|
||||
private Integer softDelete;
|
||||
|
||||
/** sim卡号 */
|
||||
private String simNo;
|
||||
|
||||
/** 设备编号 */
|
||||
private String sncode;
|
||||
|
||||
/** 绑定状态 */
|
||||
private Integer state;
|
||||
|
||||
/** msisdn */
|
||||
private String msisdn;
|
||||
|
||||
/** iccid */
|
||||
private String iccid;
|
||||
|
||||
/** imsi码 */
|
||||
private String imsi;
|
||||
|
||||
/** 本月流量 */
|
||||
private BigDecimal dataUsage;
|
||||
|
||||
/** 运营商 */
|
||||
private String carrier;
|
||||
|
||||
/** 套餐大小 */
|
||||
private Integer dataPlan;
|
||||
|
||||
/** 卡状态 */
|
||||
private Integer accountStatus;
|
||||
|
||||
/** 计费结束日期 */
|
||||
private Integer expiryDate;
|
||||
|
||||
/** 激活状态 0,1 */
|
||||
private Integer active;
|
||||
|
||||
/** 激活日期 */
|
||||
private Integer activeDate;
|
||||
|
||||
/** 剩余流量,单位M */
|
||||
private BigDecimal dataBalance;
|
||||
|
||||
/** 出库日期 */
|
||||
private Integer outboundDate;
|
||||
|
||||
/** 区域/站id */
|
||||
private Integer districtId;
|
||||
|
||||
/** 所属物业 */
|
||||
private Long propertyId;
|
||||
|
||||
/** 代理商id */
|
||||
private Long agentId;
|
||||
|
||||
/** 楼id */
|
||||
private Long buildingId;
|
||||
|
||||
/** 位置id */
|
||||
private Long positionId;
|
||||
|
||||
/** 厕所id */
|
||||
private Long toiletId;
|
||||
|
||||
/** 厂商类型0:厂商1;1:厂商2.... */
|
||||
private Integer factoryType;
|
||||
|
||||
/** 卡状态 */
|
||||
private String status;
|
||||
|
||||
/** 所属机构名称 */
|
||||
private String orgName;
|
||||
|
||||
/** 卡当前套餐名称 */
|
||||
private String iratePlanName;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package me.iiv.agent.entity.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
@@ -15,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -28,15 +29,10 @@ public class Properyt implements Serializable {
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 销售id
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 销售名称
|
||||
*/
|
||||
private String createName;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@@ -62,11 +58,6 @@ public class Properyt implements Serializable {
|
||||
*/
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 金蝶客户名称
|
||||
*/
|
||||
private String jinideName;
|
||||
|
||||
/**
|
||||
* 企业名称
|
||||
*/
|
||||
@@ -77,11 +68,6 @@ public class Properyt implements Serializable {
|
||||
*/
|
||||
private Long agentId;
|
||||
|
||||
/**
|
||||
* 代理名称
|
||||
*/
|
||||
private String agentName;
|
||||
|
||||
/**
|
||||
* 企业地址
|
||||
*/
|
||||
@@ -134,7 +120,10 @@ public class Properyt implements Serializable {
|
||||
|
||||
/**
|
||||
* 客户id自动生成
|
||||
* 注意:库中列名为驼峰 customerId(dev/prod 一致),需显式指定,
|
||||
* 否则 MyBatis-Plus 默认驼峰转下划线会映射成不存在的 customer_id。
|
||||
*/
|
||||
@TableField("customerId")
|
||||
private String customerId;
|
||||
|
||||
/**
|
||||
@@ -178,14 +167,14 @@ public class Properyt implements Serializable {
|
||||
private String invoiceName;
|
||||
|
||||
/**
|
||||
* 金蝶客户编号
|
||||
* 身份编码
|
||||
*/
|
||||
private String identifier;
|
||||
|
||||
/**
|
||||
* 金蝶内码
|
||||
* 金蝶客户名称
|
||||
*/
|
||||
private Long jindieInternalId;
|
||||
private String jinideName;
|
||||
|
||||
/**
|
||||
* 开票通讯地址
|
||||
@@ -197,6 +186,21 @@ public class Properyt implements Serializable {
|
||||
*/
|
||||
private String invoiceContact;
|
||||
|
||||
/**
|
||||
* 金蝶内码
|
||||
*/
|
||||
private Long jindieInternalId;
|
||||
|
||||
/**
|
||||
* 销售名称
|
||||
*/
|
||||
private String createName;
|
||||
|
||||
/**
|
||||
* 代理名称
|
||||
*/
|
||||
private String agentName;
|
||||
|
||||
/**
|
||||
* 最后开票时间
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -43,7 +43,7 @@ public class User implements Serializable {
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态(0:启用:1:删除)
|
||||
* 数据状态
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
@@ -72,6 +72,11 @@ public class User implements Serializable {
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 代理ID
|
||||
*/
|
||||
private Long agentId;
|
||||
|
||||
/**
|
||||
* 所属公司名称
|
||||
*/
|
||||
@@ -103,7 +108,7 @@ public class User implements Serializable {
|
||||
private Integer approle;
|
||||
|
||||
/**
|
||||
* 系统权限0:管理权限;1:普通权限
|
||||
* 角色类别(管理员,主管,其他)
|
||||
*/
|
||||
private Integer roleType;
|
||||
|
||||
@@ -112,11 +117,6 @@ public class User implements Serializable {
|
||||
*/
|
||||
private Long districtId;
|
||||
|
||||
/**
|
||||
* 代理ID
|
||||
*/
|
||||
private Long agentId;
|
||||
|
||||
/**
|
||||
* 物业ID
|
||||
*/
|
||||
@@ -128,7 +128,7 @@ public class User implements Serializable {
|
||||
private Long supervisorId;
|
||||
|
||||
/**
|
||||
* 考勤状态0:上班;1:下班;2:请假;3休息
|
||||
* 工作状态
|
||||
*/
|
||||
private Integer workStatus;
|
||||
|
||||
@@ -142,8 +142,6 @@ public class User implements Serializable {
|
||||
*/
|
||||
private String email;
|
||||
|
||||
private String emailMask;
|
||||
|
||||
/**
|
||||
* 身份证号
|
||||
*/
|
||||
@@ -175,7 +173,7 @@ public class User implements Serializable {
|
||||
private Integer backHardFlag;
|
||||
|
||||
/**
|
||||
* 权限id
|
||||
* 权限组id
|
||||
*/
|
||||
private Long authorityId;
|
||||
|
||||
@@ -227,7 +225,7 @@ public class User implements Serializable {
|
||||
private String modifyName;
|
||||
|
||||
/**
|
||||
* 物业权限id
|
||||
* 物业平台权限组id
|
||||
*/
|
||||
private Long propertyAuthorityId;
|
||||
|
||||
@@ -237,48 +235,24 @@ public class User implements Serializable {
|
||||
private String dingUserId;
|
||||
|
||||
/**
|
||||
* 工作中状态0:作业;1:转场;2:空闲;3:休息
|
||||
* 工作状态
|
||||
*/
|
||||
private Integer workAtStatus;
|
||||
|
||||
/**
|
||||
* 年龄
|
||||
*/
|
||||
private Integer age;
|
||||
|
||||
/**
|
||||
* 试用期保护0:未启用;1:启用
|
||||
*/
|
||||
private Integer probationProtection;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String note;
|
||||
|
||||
/**
|
||||
* 上班时间
|
||||
*/
|
||||
private String onTime;
|
||||
|
||||
/**
|
||||
* 下班时间
|
||||
*/
|
||||
private String offTime;
|
||||
|
||||
/**
|
||||
* 出纸机权限
|
||||
*/
|
||||
private Integer czjAuth;
|
||||
|
||||
/**
|
||||
* 空感权限
|
||||
*/
|
||||
private Integer kgAuth;
|
||||
|
||||
/**
|
||||
* 信标权限
|
||||
*/
|
||||
private Integer xbAuth;
|
||||
|
||||
/**
|
||||
@@ -311,16 +285,6 @@ public class User implements Serializable {
|
||||
*/
|
||||
private Integer productOpAuth;
|
||||
|
||||
/**
|
||||
* 金蝶人员销售订单编号
|
||||
*/
|
||||
private String jindieSalerid;
|
||||
|
||||
/**
|
||||
* 泡沫机APP显示0:关闭;1:打开;
|
||||
*/
|
||||
private Integer foamFunctionAuth;
|
||||
|
||||
/**
|
||||
* 金蝶内码
|
||||
*/
|
||||
@@ -351,6 +315,16 @@ public class User implements Serializable {
|
||||
*/
|
||||
private Integer flowAuth;
|
||||
|
||||
/**
|
||||
* 泡沫机APP显示0:关闭;1:打开;
|
||||
*/
|
||||
private Integer foamFunctionAuth;
|
||||
|
||||
/**
|
||||
* 金蝶人员销售订单编号
|
||||
*/
|
||||
private String jindieSalerid;
|
||||
|
||||
/**
|
||||
* xiaoqu-ai 用户ID
|
||||
*/
|
||||
@@ -360,44 +334,4 @@ public class User implements Serializable {
|
||||
* 同步到 xiaoqu-ai 的时间
|
||||
*/
|
||||
private Date xiaoquAiSyncedAt;
|
||||
|
||||
/**
|
||||
* 0待回填/1已解析/2歧义/3悬空;property_id非空→初始UPDATE=1
|
||||
*/
|
||||
private Byte tenantResolveStatus;
|
||||
|
||||
/**
|
||||
* 0正常/1身份待核(冲突fail-closed锁B3人证比对;⊥tenant_resolve_status,不进PEP单路)
|
||||
*/
|
||||
private Byte idcardResolveStatus;
|
||||
|
||||
/**
|
||||
* AES-256-GCM(IV‖ct‖tag);canonical后加密;明文绝不落盘
|
||||
*/
|
||||
private byte[] idcardEnc;
|
||||
|
||||
/**
|
||||
* keyed HMAC-SHA256(canonical idcard);等值/dedup/唯一
|
||||
*/
|
||||
private byte[] idcardBidx;
|
||||
|
||||
/**
|
||||
* 存储预算掩码 前3后4(默认零解密展示)
|
||||
*/
|
||||
private String idcardMask;
|
||||
|
||||
/**
|
||||
* AES-256-GCM
|
||||
*/
|
||||
private byte[] mobileEnc;
|
||||
|
||||
/**
|
||||
* keyed HMAC-SHA256(canonical mobile);等值查找(非唯一)
|
||||
*/
|
||||
private byte[] mobileBidx;
|
||||
|
||||
/**
|
||||
* 展示掩码 前3后4
|
||||
*/
|
||||
private String mobileMask;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package me.iiv.agent.entity.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 订单(m1 商城库)
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("mall_goods_order")
|
||||
public class MallGoodsOrder implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 销售id */
|
||||
private Long createId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 修改id */
|
||||
private Long modifyId;
|
||||
|
||||
/** 修改时间 */
|
||||
private Date modifyTime;
|
||||
|
||||
/** 数据状态(0:启用,99:停用) */
|
||||
private Integer softDelete;
|
||||
|
||||
/** 代理ID */
|
||||
private Long agentId;
|
||||
|
||||
/** 物业ID */
|
||||
private Long propertyId;
|
||||
|
||||
/** 站点ID */
|
||||
private Long districtId;
|
||||
|
||||
/** 客户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 订单编号 */
|
||||
private String orderNo;
|
||||
|
||||
/** 订单类型(0:代理;1:物业) */
|
||||
private Integer orderType;
|
||||
|
||||
/** 结账类型(0:预付;1:月结) */
|
||||
private Integer accountsType;
|
||||
|
||||
/** 支付方式 */
|
||||
private Integer paymentType;
|
||||
|
||||
/** 收货地址 */
|
||||
private Long addressId;
|
||||
|
||||
/** 订单状态(0:待付款;1:待出库;2:待发货;3:已发货;4:已签收;5:已收货;6:已取消;7:已删除) */
|
||||
private Integer orderState;
|
||||
|
||||
/** 支付金额 */
|
||||
private BigDecimal payMoney;
|
||||
|
||||
/** 已支付金额 */
|
||||
private BigDecimal paidMoney;
|
||||
|
||||
/** 未支付金额 */
|
||||
private BigDecimal unPayMoney;
|
||||
|
||||
/** 回款状态(0:待付款;1:已付款;2:付款中) */
|
||||
private Integer payState;
|
||||
|
||||
/** 付款付款确认状态(0:未确认;1:已确认;2:延迟确认) */
|
||||
private Integer orderPayState;
|
||||
|
||||
/** 退款状态(0:待退款;1:退款成功;2:退款失败;99:未知) */
|
||||
private Integer rePayState;
|
||||
|
||||
/** 发票状态(0:未开票;1:已开票;2:部分开票) */
|
||||
private Integer orderInvoiceState;
|
||||
|
||||
/** 订单有效时长(小时) */
|
||||
private Integer effectiveTime;
|
||||
|
||||
/** 退款原因 */
|
||||
private String reMemo;
|
||||
|
||||
/** 邮费 */
|
||||
private BigDecimal postageMoney;
|
||||
|
||||
/** 订单备注 */
|
||||
private String memo;
|
||||
|
||||
/** 应支付总金额 */
|
||||
private BigDecimal totalMoney;
|
||||
|
||||
/** 商品金额 */
|
||||
private BigDecimal goodsMoney;
|
||||
|
||||
/** 支付编号 */
|
||||
private String payNo;
|
||||
|
||||
/** 下单时间 */
|
||||
private Date orderTime;
|
||||
|
||||
/** 实际回款时间 */
|
||||
private Date payTime;
|
||||
|
||||
/** 收货时间 */
|
||||
private Date receiptTime;
|
||||
|
||||
/** 签收时间 */
|
||||
private Date signTime;
|
||||
|
||||
/** 商品类型(0:非月结商品;1:月结商品) */
|
||||
private Integer goodsType;
|
||||
|
||||
/** 账单编号(月结商品有) */
|
||||
private String billNo;
|
||||
|
||||
/** 付款识别号(线下付款) */
|
||||
private String payIdentifier;
|
||||
|
||||
/** 如果自行配送则有值 */
|
||||
private String mobile;
|
||||
|
||||
/** 特许状态(0:未特许;1:已特许) */
|
||||
private Integer franchiseState;
|
||||
|
||||
/** 优惠金额 */
|
||||
private BigDecimal discountMoney;
|
||||
|
||||
/** 活动名称 */
|
||||
private String activityName;
|
||||
|
||||
/** 活动编号 */
|
||||
private String activityNo;
|
||||
|
||||
/** 促销编号 */
|
||||
private String schemeNo;
|
||||
|
||||
/** 出库状态(0:未出库;1:已出库;2:部分出库) */
|
||||
private Integer warehouseState;
|
||||
|
||||
/** 预订单编号 */
|
||||
private String beforehandOrderNo;
|
||||
|
||||
/** 锁定状态(0:未锁定;1:已锁定) */
|
||||
private Integer lockState;
|
||||
|
||||
/** 预订单下单人 */
|
||||
private String placingPeople;
|
||||
|
||||
/** 预订单下单人 */
|
||||
private Long placingPeopleId;
|
||||
|
||||
/** 代理 */
|
||||
private String agentName;
|
||||
|
||||
/** 物业 */
|
||||
private String propertyName;
|
||||
|
||||
/** 站点 */
|
||||
private String districtName;
|
||||
|
||||
/** 下单人 */
|
||||
private String userName;
|
||||
|
||||
/** 销售名称 */
|
||||
private String createName;
|
||||
|
||||
/** 原始应支付金额 */
|
||||
private BigDecimal originalPayMoney;
|
||||
|
||||
/** 逾期标识0:否;1:是 */
|
||||
private Integer overdueFlag;
|
||||
|
||||
/** 逾期天数 */
|
||||
private Integer overdueDays;
|
||||
|
||||
/** 回款周期(天) */
|
||||
private Integer paymentCollection;
|
||||
|
||||
/** 最晚应回款时间 */
|
||||
private Date lastPayTime;
|
||||
|
||||
/** 出库日期 */
|
||||
private Date deliveryDate;
|
||||
|
||||
/** 金蝶系统内码 */
|
||||
private Long jindieInternalId;
|
||||
|
||||
/** 金蝶系统采购订单内码 */
|
||||
private Long jindiePurchaseInternalId;
|
||||
|
||||
/** 销售公司 */
|
||||
private Long saleCompanyId;
|
||||
|
||||
/** 销售公司 */
|
||||
private String saleCompany;
|
||||
|
||||
/** 发货公司 */
|
||||
private Long deliveryCompanyId;
|
||||
|
||||
/** 发货公司 */
|
||||
private String deliveryCompany;
|
||||
|
||||
/** 已开票金额 */
|
||||
private BigDecimal invoiceMoney;
|
||||
|
||||
/** 未开票金额 */
|
||||
private BigDecimal unInvoiceMoney;
|
||||
|
||||
/** 金蝶销售服务订单内码 */
|
||||
private Long jindieServiceOrderInternalId;
|
||||
|
||||
/** 服务订单状态(0:未处理;1:已处理;2:无需处理) */
|
||||
private Integer serviceOrderState;
|
||||
|
||||
/** 微店订单标识(0:否;1:是) */
|
||||
private Integer weiDianOrderFlag;
|
||||
|
||||
/** 微店订单编号 */
|
||||
private String weiDianOrderNo;
|
||||
|
||||
/** 装机单标识(0:否;1:是) */
|
||||
private Integer installOrderFlag;
|
||||
|
||||
/** 营销状态(0:未营销;1:部分营销;2:已营销) */
|
||||
private Integer marketingState;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package me.iiv.agent.entity.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 订单商品详情(m1 商城库)
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("mall_order_details")
|
||||
public class MallOrderDetails implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 创建id */
|
||||
private Long createId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 修改id */
|
||||
private Long modifyId;
|
||||
|
||||
/** 修改时间 */
|
||||
private Date modifyTime;
|
||||
|
||||
/** 数据状态(0:启用,99:停用) */
|
||||
private Integer softDelete;
|
||||
|
||||
/** spu编号 */
|
||||
private String spuNo;
|
||||
|
||||
/** sku编号 */
|
||||
private String skuNo;
|
||||
|
||||
/** 购买数量 */
|
||||
private Integer count;
|
||||
|
||||
/** 单价 */
|
||||
private BigDecimal price;
|
||||
|
||||
/** 折扣 */
|
||||
private Double discount;
|
||||
|
||||
/** 订单编号 */
|
||||
private String orderNo;
|
||||
|
||||
/** 商品类型(0:非月结商品;1:月结商品) */
|
||||
private Integer goodsType;
|
||||
|
||||
/** sku标签 */
|
||||
private String skuLable;
|
||||
|
||||
/** 商品描述 */
|
||||
private String skuDesc;
|
||||
|
||||
/** 封面路径 */
|
||||
private String picUrl;
|
||||
|
||||
/** spu名称 */
|
||||
private String spuName;
|
||||
|
||||
/** 物料类型(0:设备;1:配件;2:耗材);历史遗留驼峰列名 */
|
||||
@TableField("spuType")
|
||||
private Integer spuType;
|
||||
|
||||
/** 出库状态(0:未出库;1:已出库) */
|
||||
private Integer warehouseState;
|
||||
|
||||
/** 出库单ID */
|
||||
private Long warehouseOutId;
|
||||
|
||||
/** 原始单价 */
|
||||
private BigDecimal originalPrice;
|
||||
|
||||
/** 原始购买数量 */
|
||||
private Integer originalCount;
|
||||
|
||||
/** 收款状态(0:未收款;1:全部收款;2:部分收款) */
|
||||
private Integer payState;
|
||||
|
||||
/** 发票状态(0:未开票;1:全部开票;2:部分开票) */
|
||||
private Integer orderInvoiceState;
|
||||
|
||||
/** 应支付金额 */
|
||||
private BigDecimal payMoney;
|
||||
|
||||
/** 应开票金额 */
|
||||
private BigDecimal invoiceMoney;
|
||||
|
||||
/** 未支付金额 */
|
||||
private BigDecimal notPayMoney;
|
||||
|
||||
/** 未开票金额 */
|
||||
private BigDecimal notInvoiceMoney;
|
||||
|
||||
/** 营销状态(0:未营销;1:营销中;2:已营销) */
|
||||
private Integer marketingState;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package me.iiv.agent.entity.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 订单物流(m1 商城库)
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("mall_order_logistics")
|
||||
public class MallOrderLogistics implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 订单发货人 */
|
||||
private Long createId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 修改id */
|
||||
private Long modifyId;
|
||||
|
||||
/** 发货时间 */
|
||||
private Date modifyTime;
|
||||
|
||||
/** 数据状态(0:启用,99:停用) */
|
||||
private Integer softDelete;
|
||||
|
||||
/** 发货方式(0:快递公司;1:自行派送) */
|
||||
private Integer sendMode;
|
||||
|
||||
/** 订单编号 */
|
||||
private String orderNo;
|
||||
|
||||
/** 物流公司id */
|
||||
private Long logisticsId;
|
||||
|
||||
/** 物流公司编号 */
|
||||
private String logisticsNo;
|
||||
|
||||
/** 物流公司名称 */
|
||||
private String logisticsName;
|
||||
|
||||
/** 快递单号 */
|
||||
private String logisticsOrderNo;
|
||||
|
||||
/** 自己生成的编号 */
|
||||
private String logisticsSelfNo;
|
||||
|
||||
/** 派送员姓名 */
|
||||
private String deliverName;
|
||||
|
||||
/** 派送手机号 */
|
||||
private String deliverNumber;
|
||||
|
||||
/** 配送员id */
|
||||
private Long userId;
|
||||
|
||||
/** 物流当前状态(0:已出单;1:已出库;2:已发货;3:已揽件;4:运输中;5:待取件;6:派送中;7:已签收) */
|
||||
private Integer state;
|
||||
|
||||
/** 收货方公司名称 */
|
||||
private String receivingGoodsName;
|
||||
|
||||
/** 收货姓名 */
|
||||
private String reveiveName;
|
||||
|
||||
/** 收货手机号 */
|
||||
private String reveiveNumber;
|
||||
|
||||
/** 详细地址 */
|
||||
private String address;
|
||||
|
||||
private Long agentId;
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package me.iiv.agent.entity.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 订单物流轨迹(m1 商城库;表名为老系统遗留拼写 mall_order_logistics_detai)。
|
||||
* 注意:logistics_order_no 字段在老系统写入时混用,实际存的是订单号。
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("mall_order_logistics_detai")
|
||||
public class MallOrderLogisticsDetai implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 创建id */
|
||||
private Long createId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 修改id */
|
||||
private Long modifyId;
|
||||
|
||||
/** 修改时间 */
|
||||
private Date modifyTime;
|
||||
|
||||
/** 数据状态(0:启用,99:停用) */
|
||||
private Integer softDelete;
|
||||
|
||||
/** 快递单号(老系统写入时混用,实际多为订单号) */
|
||||
private String logisticsOrderNo;
|
||||
|
||||
/** 物流详情 */
|
||||
private String memo;
|
||||
|
||||
/** 物流状态(0:已出单;1:已出库;2:已发货;3:已揽件;4:运输中;5:待取件;6:派送中;7:已签收;13:上传凭证) */
|
||||
private Integer state;
|
||||
|
||||
/** 订单号 */
|
||||
private String orderNo;
|
||||
|
||||
/** 图片列表 */
|
||||
private String picList;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package me.iiv.agent.entity.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 收货地址(m1 商城库)
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("mall_shipping_address")
|
||||
public class MallShippingAddress implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 创建id */
|
||||
private Long createId;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createTime;
|
||||
|
||||
/** 修改id */
|
||||
private Long modifyId;
|
||||
|
||||
/** 修改时间 */
|
||||
private Date modifyTime;
|
||||
|
||||
/** 数据状态(0:启用,99:停用) */
|
||||
private Integer softDelete;
|
||||
|
||||
/** 收货姓名 */
|
||||
private String reveiveName;
|
||||
|
||||
/** 收货手机号 */
|
||||
private String reveiveNumber;
|
||||
|
||||
/** 客户id */
|
||||
private Long userId;
|
||||
|
||||
/** 下单人 */
|
||||
private String userName;
|
||||
|
||||
/** 所属站点 */
|
||||
private Long districtId;
|
||||
|
||||
/** 省 */
|
||||
private String province;
|
||||
|
||||
/** 市 */
|
||||
private String city;
|
||||
|
||||
/** 县 */
|
||||
private String countyseat;
|
||||
|
||||
/** 详细地址 */
|
||||
private String address;
|
||||
|
||||
/** 备注信息 */
|
||||
private String memo;
|
||||
|
||||
/** 状态 */
|
||||
private Integer status;
|
||||
|
||||
/** 代理ID */
|
||||
private Long agentId;
|
||||
|
||||
/** 默认标识0:否;1:是 */
|
||||
private Integer defaultFlag;
|
||||
|
||||
/** 使用客户类型(0:内部人员;1:渠道商;2:直营客户) */
|
||||
private Integer useCustomerType;
|
||||
|
||||
/** 使用客户ID */
|
||||
private Long useCustomerId;
|
||||
|
||||
/** 使用客户名称 */
|
||||
private String useCustomerName;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同附件
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("contract_file")
|
||||
public class ContractFile implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改id
|
||||
*/
|
||||
private Long modifyId;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态0:正常;1:删除
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
/**
|
||||
* 合同ID(0=上传后未提交的孤儿附件,定时清理)
|
||||
*/
|
||||
private Long contractId;
|
||||
|
||||
/**
|
||||
* 原始文件名
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件访问地址
|
||||
*/
|
||||
private String fileUrl;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同主表
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("contract_new")
|
||||
public class ContractNew implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改id
|
||||
*/
|
||||
private Long modifyId;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态0:正常;1:删除
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
/**
|
||||
* 合同编号(properyt.serialnumber + "C" + 三位流水,如452542C001)
|
||||
*/
|
||||
private String contractNo;
|
||||
|
||||
/**
|
||||
* 合同名称
|
||||
*/
|
||||
private String contractName;
|
||||
|
||||
/**
|
||||
* 合同所属公司(=物业所属销售所属公司,审批流按此公司匹配)
|
||||
*/
|
||||
private Long contractAgentId;
|
||||
|
||||
/**
|
||||
* 客户所属公司(=创建人提交时选择的所属公司)
|
||||
*/
|
||||
private Long customerAgentId;
|
||||
|
||||
/**
|
||||
* 所属物业ID(关联权限库properyt)
|
||||
*/
|
||||
private Long propertyId;
|
||||
|
||||
/**
|
||||
* 物业名称(冗余快照)
|
||||
*/
|
||||
private String propertyName;
|
||||
|
||||
/**
|
||||
* 客户编号快照(=properyt.serialnumber,编号流水/按客户归集用)
|
||||
*/
|
||||
private String customerNo;
|
||||
|
||||
/**
|
||||
* 客户名称快照(=properyt.jinide_name 金蝶客户名称)
|
||||
*/
|
||||
private String customerName;
|
||||
|
||||
/**
|
||||
* 合同金额(元)
|
||||
*/
|
||||
private BigDecimal amount;
|
||||
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
private String contact;
|
||||
|
||||
/**
|
||||
* 合同开始日期
|
||||
*/
|
||||
private Date startDate;
|
||||
|
||||
/**
|
||||
* 合同结束日期
|
||||
*/
|
||||
private Date endDate;
|
||||
|
||||
/**
|
||||
* 到期提醒开关0:关;1:开
|
||||
*/
|
||||
private Integer expireRemind;
|
||||
|
||||
/**
|
||||
* 到期提醒天数
|
||||
*/
|
||||
private Integer remindDays;
|
||||
|
||||
/**
|
||||
* 到期提醒时间(=end_date-remind_days 当天09:30:00)
|
||||
*/
|
||||
private Date remindTime;
|
||||
|
||||
/**
|
||||
* 到期提醒是否已发送0:未发送;1:已发送
|
||||
*/
|
||||
private Integer reminded;
|
||||
|
||||
/**
|
||||
* 所属销售ID(=物业所属销售)
|
||||
*/
|
||||
private Long saleId;
|
||||
|
||||
/**
|
||||
* 所属销售姓名(冗余快照)
|
||||
*/
|
||||
private String saleName;
|
||||
|
||||
/**
|
||||
* 合同状态0:待审;1:驳回;2:正常;3:过期;4:终止
|
||||
*/
|
||||
private Integer contractStatus;
|
||||
|
||||
/**
|
||||
* 终止原因分类(cancel_reason.content快照,type=6)
|
||||
*/
|
||||
private String stopReasonType;
|
||||
|
||||
/**
|
||||
* 终止详细原因
|
||||
*/
|
||||
private String stopReason;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同流程/待办/操作记录表
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("contract_process")
|
||||
public class ContractProcess implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 创建id
|
||||
*/
|
||||
private Long createId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 修改id
|
||||
*/
|
||||
private Long modifyId;
|
||||
|
||||
/**
|
||||
* 修改时间
|
||||
*/
|
||||
private Date modifyTime;
|
||||
|
||||
/**
|
||||
* 数据状态0:正常;1:删除
|
||||
*/
|
||||
private Integer softDelete;
|
||||
|
||||
/**
|
||||
* 合同ID
|
||||
*/
|
||||
private Long contractId;
|
||||
|
||||
/**
|
||||
* 合同编号(冗余)
|
||||
*/
|
||||
private String contractNo;
|
||||
|
||||
/**
|
||||
* 节点类型0:创建;1:部门内审核;2:层级审核;3:终止;4:抄送
|
||||
*/
|
||||
private Integer nodeType;
|
||||
|
||||
/**
|
||||
* 审核层级(层级审核为第几级,其余0)
|
||||
*/
|
||||
private Integer auditLevel;
|
||||
|
||||
/**
|
||||
* 操作类型0:创建;1:审核通过;2:审核驳回;3:终止;4:修改再提交;5:抄送
|
||||
*/
|
||||
private Integer operationType;
|
||||
|
||||
/**
|
||||
* 待办人/操作人ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 待办人/操作人姓名
|
||||
*/
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 待办状态0:无需待办;1:待办;2:已办
|
||||
*/
|
||||
private Integer scheduleState;
|
||||
|
||||
/**
|
||||
* 备注/驳回原因/终止原因/抄送文案
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 办理时间(待办未办为NULL)
|
||||
*/
|
||||
private Date operateTime;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 调拨/采购/划转流程视图 warehouse_allocation_process(m2,只读):一行 = 单据 × 处理人 × 待办环节。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_allocation_process")
|
||||
public class WarehouseAllocationProcess implements Serializable {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long warehouseAllocationId;
|
||||
private String allocationOrderNo;
|
||||
private Integer allocationCount;
|
||||
private Date applyDate;
|
||||
private String applyUserName;
|
||||
private Long applyUserId;
|
||||
/** 视图列名即 currend_audit_level(老系统拼写保留) */
|
||||
@TableField("currend_audit_level")
|
||||
private Integer currendAuditLevel;
|
||||
private Integer goodsCount;
|
||||
private LocalDate allocationDate;
|
||||
private String userName;
|
||||
private Long userId;
|
||||
/** 操作主题(0:审核通过;1:审核驳回;2:入库确认驳回;3:转出确认;4:出库确认驳回;5:驳回后修改提交;6:入库确认;7:出库确认;8:撤销) */
|
||||
private Integer operationTheme;
|
||||
/** 待办状态(0:无需待办;1:待办;2:已办) */
|
||||
private Integer scheduleState;
|
||||
/** 审核状态(0:审核驳回/驳回;1:审核通过/出库确认) */
|
||||
private Integer auditState;
|
||||
/** 流程行审核级别 */
|
||||
@TableField("process_audit_level")
|
||||
private Integer processAuditLevel;
|
||||
/** 操作类型(0:审批流操作;1:确认入库操作;2:确认出库操作;3:本人修改;4:撤销,纯日志) */
|
||||
private Integer operationType;
|
||||
private String warehouseOutName;
|
||||
private String warehouseInName;
|
||||
private Long warehouseInId;
|
||||
private Long warehouseOutId;
|
||||
/** 单据审核级别 */
|
||||
private Integer auditLevel;
|
||||
/** 调拨单状态(0:待审核;1:被驳回;2:待出库;3:待入库;4:已完成;5:已撤销;6:作废) */
|
||||
private Integer orderState;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Date createTime;
|
||||
/** 办理类型(0:调拨;1:采购;2:划转) */
|
||||
private Integer opType;
|
||||
private String memo;
|
||||
private String inUserName;
|
||||
private Date inTime;
|
||||
private String nextUserName;
|
||||
private String outUserName;
|
||||
private Date outTime;
|
||||
private String pickupOrderNo;
|
||||
private String installOrderNo;
|
||||
private String deliveryOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 入库单主表(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in")
|
||||
public class WarehouseIn implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private String inOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private LocalDate inDate;
|
||||
private Integer goodsCount;
|
||||
private Integer inCount;
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyDate;
|
||||
private String memo;
|
||||
private Integer auditLevel;
|
||||
private Integer currendAuditLevel;
|
||||
private Integer inSynchronizeState;
|
||||
private Date inSynchronizeTime;
|
||||
private String removeUserName;
|
||||
private Date removeDate;
|
||||
private String outOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long logisticsId;
|
||||
private String logisticsName;
|
||||
private String logisticsWayBill;
|
||||
private String deliveryOrderNo;
|
||||
private String installOrderNo;
|
||||
private String pickupOrderNo;
|
||||
private String withdrawOrderNo;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/** 入库单审批/处理流程(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in_audit_process")
|
||||
public class WarehouseInAuditProcess implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long warehouseInId;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private Date operationTime;
|
||||
private Integer scheduleState;
|
||||
private Integer auditState;
|
||||
private Integer auditLevel;
|
||||
private Integer operationTheme;
|
||||
private Integer operationType;
|
||||
private String memo;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 入库单物料/SN 明细(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in_details")
|
||||
public class WarehouseInDetail implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private String inOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private Long warehouseInId;
|
||||
private Long materielDefinitionId;
|
||||
private String materialNo;
|
||||
private String materielFullName;
|
||||
private String materielSort;
|
||||
private Long materielIndexId;
|
||||
private String specifications;
|
||||
private String snCode;
|
||||
private Integer inCount;
|
||||
private Integer currentInventory;
|
||||
private LocalDate inDate;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Integer goodInState;
|
||||
private Date inConfirmTime;
|
||||
private String inConfirmUserName;
|
||||
private String outOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Integer assetDisposalType;
|
||||
private Integer synchronizeState;
|
||||
private Long outDetailsId;
|
||||
private String deliveryOrderNo;
|
||||
private String installOrderNo;
|
||||
private String pickupOrderNo;
|
||||
private String withdrawOrderNo;
|
||||
private String memo;
|
||||
private String lineMemo;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** 出入库附件定位信息(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in_out_pic")
|
||||
public class WarehouseInOutPic implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long aggregateId;
|
||||
private String fileName;
|
||||
private String ossbucketname;
|
||||
private String ossfolder;
|
||||
private Integer fileType;
|
||||
private Integer inOutAllocationType;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 入库单流程视图 warehouse_in_process(m2,只读):一行 = 单据 × 处理人 × 待办环节。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in_process")
|
||||
public class WarehouseInProcess implements Serializable {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long warehouseInId;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private Date operationTime;
|
||||
/** 待办状态(0:无需待办;1:待办;2:已办) */
|
||||
private Integer scheduleState;
|
||||
/** 审核状态(0:审核驳回;1:审核通过),仅与流程审批有关 */
|
||||
private Integer auditState;
|
||||
/** 操作主题(0:审核通过;1:审核驳回;2:撤销入库;3:入库确认;4:驳回修改提交;5:撤销;6:入库驳回) */
|
||||
private Integer operationTheme;
|
||||
/** 操作类型(0:审批流操作;1:确认入库操作;2:本人修改;3:驳回或修改操作,纯日志) */
|
||||
private Integer operationType;
|
||||
private String inOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private String outOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private LocalDate inDate;
|
||||
private Integer goodsCount;
|
||||
private Integer inCount;
|
||||
/** 入库单状态(0:待审核;1:被驳回;2:待入库;3:已完成;4:已撤销;5:作废) */
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyDate;
|
||||
private Integer auditLevel;
|
||||
/** 视图列名即 currend_audit_level(老系统拼写保留) */
|
||||
@TableField("currend_audit_level")
|
||||
private Integer currendAuditLevel;
|
||||
private String memo;
|
||||
private Date createTime;
|
||||
private String pickupOrderNo;
|
||||
private String installOrderNo;
|
||||
private String deliveryOrderNo;
|
||||
private String withdrawOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 出库单主表(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_out")
|
||||
public class WarehouseOut implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private String outOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private LocalDate outDate;
|
||||
private Integer goodsCount;
|
||||
private Integer outCount;
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyDate;
|
||||
private String memo;
|
||||
private Integer auditLevel;
|
||||
private Integer currendAuditLevel;
|
||||
private Integer useCustomerType;
|
||||
private Long useCustomerId;
|
||||
private String useCustomerName;
|
||||
private Integer outSynchronizeState;
|
||||
private Date outSynchronizeTime;
|
||||
private String removeUserName;
|
||||
private Date removeDate;
|
||||
private Integer sendMode;
|
||||
private Long logisticsId;
|
||||
private String logisticsName;
|
||||
private String logisticsOrderNo;
|
||||
private String logisticsWayBill;
|
||||
private Long addressId;
|
||||
private Integer assetSynchronizeState;
|
||||
private String deliveryOrderNo;
|
||||
private String installOrderNo;
|
||||
private String pickupOrderNo;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/** 出库单审批/处理记录(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_out_audit_process")
|
||||
public class WarehouseOutAuditProcess implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long warehouseOutId;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private Date operationTime;
|
||||
private Integer scheduleState;
|
||||
private Integer auditState;
|
||||
private Integer auditLevel;
|
||||
private Integer operationTheme;
|
||||
private Integer operationType;
|
||||
private String memo;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/** 出库凭证备注(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_out_certificate")
|
||||
public class WarehouseOutCertificate implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long outOrderId;
|
||||
private String outOrderNo;
|
||||
private String memo;
|
||||
private Date createTime;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 出库单物料/SN 明细(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_out_details")
|
||||
public class WarehouseOutDetail implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Integer outCount;
|
||||
private String outOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long warehouseOutId;
|
||||
private Long materielDefinitionId;
|
||||
private String materialNo;
|
||||
private String materielFullName;
|
||||
private String materielSort;
|
||||
private Long materielIndexId;
|
||||
private String specifications;
|
||||
private String snCode;
|
||||
private LocalDate outDate;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private Integer orderState;
|
||||
private Integer queryFlag;
|
||||
private Long parentId;
|
||||
private Date outConfirmTime;
|
||||
private String outConfirmUserName;
|
||||
private BigDecimal price;
|
||||
private BigDecimal allMoney;
|
||||
private String threeAttribute;
|
||||
private String assetDisposalType;
|
||||
private Integer assetSynchronizeState;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 出库单流程视图 warehouse_out_process(m2,只读):一行 = 单据 × 处理人 × 待办环节。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_out_process")
|
||||
public class WarehouseOutProcess implements Serializable {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long warehouseOutId;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private Date operationTime;
|
||||
/** 待办状态(0:无需待办;1:待办;2:已办) */
|
||||
private Integer scheduleState;
|
||||
/** 审核状态(0:审核驳回;1:审核通过),仅与流程审批有关 */
|
||||
private Integer auditState;
|
||||
/** 操作主题(0:审核通过;1:审核驳回;2:撤销入库;3:出库确认;4:驳回修改提交;5:撤销) */
|
||||
private Integer operationTheme;
|
||||
/** 操作类型(0:审批流操作;1:确认出库操作;2:本人修改;3:驳回或修改操作,纯日志) */
|
||||
private Integer operationType;
|
||||
private String outOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private LocalDate outDate;
|
||||
private Integer goodsCount;
|
||||
private Integer outCount;
|
||||
/** 出库单状态(0:待审核;1:被驳回;2:待出库;3:已完成;4:已撤销;5:作废) */
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyDate;
|
||||
private Integer auditLevel;
|
||||
/** 视图列名即 currend_audit_level(老系统拼写保留) */
|
||||
@TableField("currend_audit_level")
|
||||
private Integer currendAuditLevel;
|
||||
private String memo;
|
||||
private Date createTime;
|
||||
private String pickupOrderNo;
|
||||
private String installOrderNo;
|
||||
private String deliveryOrderNo;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** 出入库类型(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_type")
|
||||
public class WarehouseType implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Integer warehouseType;
|
||||
private String typeName;
|
||||
private Integer sceneType;
|
||||
private String typeNo;
|
||||
private Integer assetFlag;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -38,7 +38,7 @@ public class AdminUser implements Serializable {
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* PBKDF2 加盐密码哈希
|
||||
* PBKDF2 加盐密码哈希
|
||||
*/
|
||||
private String passwordHash;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -26,6 +26,12 @@ public class AgentApiKey implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 主体类型:用户委托(绑定 user_id,行级权限跟随人员) */
|
||||
public static final String PRINCIPAL_TYPE_USER = "user";
|
||||
|
||||
/** 主体类型:租户服务(Open API 服务密钥签发,不绑定人员) */
|
||||
public static final String PRINCIPAL_TYPE_SERVICE = "service";
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@@ -33,10 +39,20 @@ public class AgentApiKey implements Serializable {
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 所属人(m0.user.id)
|
||||
* 所属人(m0.user.id);principal_type=service 时为 NULL
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 主体类型:user=用户委托 service=租户服务
|
||||
*/
|
||||
private String principalType;
|
||||
|
||||
/**
|
||||
* 服务主体标识(principal_type=service 时必填,如 xiaoqu-ai)
|
||||
*/
|
||||
private String subjectName;
|
||||
|
||||
/**
|
||||
* ak 前缀(展示用,如 xqak_abcd1234)
|
||||
*/
|
||||
@@ -48,7 +64,7 @@ public class AgentApiKey implements Serializable {
|
||||
private String keyHash;
|
||||
|
||||
/**
|
||||
* ak 明文 AES-GCM 加密密文(二次获取用),空=历史数据未存
|
||||
* ak 明文 AES-GCM 加密密文(Base64: iv‖cipher‖tag),空=历史数据未存
|
||||
*/
|
||||
private String keyCipher;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package me.iiv.agent.entity.svc;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("app_open_client")
|
||||
public class AppOpenClient implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String keyPrefix;
|
||||
private String keyHash;
|
||||
private String memo;
|
||||
private Byte status;
|
||||
private Date lastUsedTime;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Byte softDelete;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package me.iiv.agent.entity.svc;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("app_open_client_scope")
|
||||
public class AppOpenClientScope implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long clientId;
|
||||
private Long scopeId;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Byte softDelete;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.Date;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import me.iiv.agent.entity.m0.AgentManageDistrictAuth;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
public interface AgentManageDistrictAuthMapper extends BaseMapper<AgentManageDistrictAuth> {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import me.iiv.agent.entity.m0.Agent;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
public interface AgentMapper extends BaseMapper<Agent> {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import me.iiv.agent.entity.m0.Authority;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
public interface AuthorityMapper extends BaseMapper<Authority> {
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m0.DistrictDistrictStrategy;
|
||||
|
||||
public interface DistrictDistrictStrategyMapper extends BaseMapper<DistrictDistrictStrategy> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m0.HardwareView;
|
||||
|
||||
public interface HardwareViewMapper extends BaseMapper<HardwareView> {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m0.InvoiceInfo;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 发票信息表 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
public interface InvoiceInfoMapper extends BaseMapper<InvoiceInfo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m0;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m0.Networkcard;
|
||||
|
||||
public interface NetworkcardMapper extends BaseMapper<Networkcard> {
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import me.iiv.agent.entity.m0.Properyt;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
public interface ProperytMapper extends BaseMapper<Properyt> {
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import me.iiv.agent.entity.m0.User;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m1.MallGoodsOrder;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* MallGoodsOrder Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
public interface MallGoodsOrderMapper extends BaseMapper<MallGoodsOrder> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m1.MallOrderDetails;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* MallOrderDetails Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
public interface MallOrderDetailsMapper extends BaseMapper<MallOrderDetails> {
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m1.MallOrderLogisticsDetai;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* MallOrderLogisticsDetai Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
public interface MallOrderLogisticsDetaiMapper extends BaseMapper<MallOrderLogisticsDetai> {
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m1.MallOrderLogistics;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* MallOrderLogistics Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
public interface MallOrderLogisticsMapper extends BaseMapper<MallOrderLogistics> {
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m1;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m1.MallShippingAddress;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* MallShippingAddress Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-09
|
||||
*/
|
||||
public interface MallShippingAddressMapper extends BaseMapper<MallShippingAddress> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.ContractFile;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同附件 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
public interface ContractFileMapper extends BaseMapper<ContractFile> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同主表 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
public interface ContractNewMapper extends BaseMapper<ContractNew> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.ContractProcess;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 合同流程/待办/操作记录 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-11
|
||||
*/
|
||||
public interface ContractProcessMapper extends BaseMapper<ContractProcess> {
|
||||
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ import me.iiv.agent.entity.m2.WarehouseAgentAuth;
|
||||
* </p>
|
||||
*
|
||||
* @author codegen
|
||||
* @since 2026-09-03
|
||||
* @since 2026-09-08
|
||||
*/
|
||||
public interface WarehouseAgentAuthMapper extends BaseMapper<WarehouseAgentAuth> {
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseAllocationProcess;
|
||||
|
||||
public interface WarehouseAllocationProcessMapper extends BaseMapper<WarehouseAllocationProcess> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseInAuditProcess;
|
||||
|
||||
public interface WarehouseInAuditProcessMapper extends BaseMapper<WarehouseInAuditProcess> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseInDetail;
|
||||
|
||||
public interface WarehouseInDetailMapper extends BaseMapper<WarehouseInDetail> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseIn;
|
||||
|
||||
public interface WarehouseInMapper extends BaseMapper<WarehouseIn> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseInOutPic;
|
||||
|
||||
public interface WarehouseInOutPicMapper extends BaseMapper<WarehouseInOutPic> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseInProcess;
|
||||
|
||||
public interface WarehouseInProcessMapper extends BaseMapper<WarehouseInProcess> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutAuditProcess;
|
||||
|
||||
public interface WarehouseOutAuditProcessMapper extends BaseMapper<WarehouseOutAuditProcess> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutCertificate;
|
||||
|
||||
public interface WarehouseOutCertificateMapper extends BaseMapper<WarehouseOutCertificate> {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user