Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,16 @@ Row-level permissions · multi-tab that keeps state · approval workflow · an A
|---|---|
| **Access & org** | RBAC (menus / buttons / data scopes) · row-level data permissions · department tree · users & roles |
| **Import & export** | server-side xlsx / csv export (current filter or selected rows · large exports run in the background and notify you when done · formula-injection safe) · generic import (templates with dropdowns · preview & validation · insert only / upsert / update only · error report · all-or-nothing) · build a whole department tree from one file |
| **Sign-in & security** | captcha · lockout · password history & expiry · OAuth2 (GitHub / Google) · force sign-out of sessions |
| **Workspace** | multi-tab (pin / drag / reload) · command palette ⌘K · shortcuts · guided tour · theme / radius / tab style · per-user time zone · Chinese & English |
| **Sign-in & security** | captcha · lockout · password history & expiry · SMS code sign-in · two-factor auth (TOTP + recovery codes) · OAuth2 (GitHub / Google) · session management (idle timeout + absolute limit · per-client session caps · tells users why they were signed out · force sign-out) |
| **Workspace** | customizable dashboard (cards appear by permission · layout saved per user on the server) · multi-tab (pin / drag / reload) · command palette ⌘K · shortcuts · guided tour · theme / radius / tab style · per-user time zone · Chinese & English |
| **Files & storage** | file manager with previews (pdf / docx / xlsx / images / archives) · local + S3-compatible storage · pre-signed downloads · recycle bin · instant upload · chunked & resumable uploads · attachment panel for any page |
| **Approval workflow** | form designer · flow tree (approve / CC / conditional branches) · approve / reject / transfer / send back / take back / add approvers · timeout reminders and auto-actions · drafts · urgent · handover |
| **Approval workflow** | form designer · flow tree (approve / CC / conditional branches) · approve / reject / transfer / send back / take back / add approvers · timeout reminders and auto-actions · drafts · urgent · handover · AI pre-review / form filling / AI approval nodes |
| **AI assistant** | full-page chat + side drawer · tool calls bound by permissions and data scopes · preview + confirm for writes · audit log · MCP gateway |
| **Open API** | AK/SK-signed endpoints · scopes / IP allow-list / replay protection / rate limits · call logs · incremental sync · Python / Node / Java SDKs |
| **Messages & jobs** | in-app notifications (real-time) · notices (rich text + attachments) · scheduled jobs (online scheduling · cron preview · run history) |
| **Open API** | AK/SK-signed endpoints · scopes / IP allow-list / replay protection / rate limits · call logs · incremental sync · Python / Node / Java SDKs · webhook subscriptions (Standard Webhooks signatures · retries · delivery log · bulk resend) |
| **Messages & notifications** | in-app notifications (real-time) · notices (rich text + attachments · audiences · scheduled publishing · pinned / level / expiry · pop-up after sign-in with required acknowledgement · top banner · sign-in page notices · read stats) · notification routing (email / SMS / Feishu · DingTalk · WeCom group bots · per-event policies · delivery log) · SMS (Alibaba Cloud / Tencent Cloud) |
| **Scheduled jobs** | online scheduling · cron preview · run history |
| **Operations** | settings · dictionaries · sign-in / operation logs (redacted) · server & Redis monitoring · new-version prompt |
| **Clients** | Electron desktop (silent printing · local hardware · auto-update) · native Expo app (not a WebView wrapper) |
| **Clients** | Electron desktop (installers for three platforms · silent printing · local hardware · auto-update) · native Expo app (not a WebView wrapper, **preview** — see below) |
| **Developers** | component sandbox (every component tweakable and copy-pasteable) · color palette · type-safe API contract generated from OpenAPI |

## Stack
Expand All @@ -83,6 +84,14 @@ Row-level permissions · multi-tab that keeps state · approval workflow · an A
| **Clients** | Electron 42 · Expo 57 / React Native 0.86 |
| **Tooling** | pnpm + turbo monorepo · uv · ruff · pytest · Playwright · GitHub Actions |

## Mobile app (preview)

The Expo app is shipped as a **preview** in 1.0 — it's real and usable, but not yet covered by the 1.0 compatibility promises.

- **Has:** home · notices · notifications · users · profile · server monitoring · time zone · SMS code sign-in · session expiry reasons
- **Not yet:** approvals · roles / departments / menus · files · operation & sign-in logs (the Apps screen lists them as "not built yet" rather than hiding them) · binding a phone number or turning on two-factor auth from the app
- **Not yet verified:** no automated tests ([#137](https://github.com/yilecoding/fastapi-react-admin/issues/137)), most screens not run on a real device, no release pipeline

## Databases

Switch with `DATABASE_TYPE` in `.env` — same code, same migration chain, seed data for all three.
Expand Down
21 changes: 15 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,16 @@
|---|---|
| **权限与组织** | RBAC(菜单 / 按钮 / 数据范围)· 行级数据权限 · 部门树 · 用户与角色 |
| **导入导出** | 服务端导出 xlsx / csv(按当前筛选或勾选的行 · 大批量转后台、完成发站内消息 · 防公式注入)· 通用导入(带下拉的模板 · 预览校验 · 只新增 / 新增并更新 / 只更新 · 错误报告 · 全部成功才导入)· 一份文件建整棵部门树 |
| **登录与安全** | 图形验证码 · 失败锁定 · 密码历史与过期 · OAuth2(GitHub / Google)· 在线会话强制下线 |
| **工作区** | 多页签(固定 / 拖拽 / 重新加载)· 命令面板 ⌘K · 快捷键 · 功能引导 · 列表列显隐与拖拽排序(写进地址栏)· 主题色 / 圆角 / 页签外观 · 每人自己的时区 · 中英双语 |
| **登录与安全** | 图形验证码 · 失败锁定 · 密码历史与过期 · 手机验证码登录 · 二次验证(TOTP + 恢复码)· OAuth2(GitHub / Google)· 会话管理(空闲超时 + 绝对上限 · 按端限制会话数 · 被踢下线说明原因 · 强制下线) |
| **工作区** | 可编辑布局的工作台(卡片按权限出现 · 布局按人存服务端)· 多页签(固定 / 拖拽 / 重新加载)· 命令面板 ⌘K · 快捷键 · 功能引导 · 列表列显隐与拖拽排序(写进地址栏)· 主题色 / 圆角 / 页签外观 · 每人自己的时区 · 中英双语 |
| **文件与存储** | 文件管理器 + 在线预览(pdf / docx / xlsx / 图片 / 压缩包)· 本地 + S3 兼容存储 · 预签名直连下载 · 回收站 · 秒传 · 分片上传与断点续传 · 可嵌进任何页面的附件面板 |
| **审批流** | 表单设计器 · 流程树(审批 / 抄送 / 条件分支)· 同意 / 驳回 / 转交 / 退回 / 拿回 / 加签 · 超时提醒与自动处理 · 草稿 · 加急 · 离职交接 |
| **审批流** | 表单设计器 · 流程树(审批 / 抄送 / 条件分支)· 同意 / 驳回 / 转交 / 退回 / 拿回 / 加签 · 超时提醒与自动处理 · 草稿 · 加急 · 离职交接 · AI 预审 / 帮填表单 / AI 审批节点 |
| **AI 助手** | 整页对话 + 侧滑抽屉 · 受权限与数据范围约束的工具调用 · 写操作预演 + 确认 · 调用审计 · MCP 网关 |
| **能力开放** | AK/SK 签名的开放接口 · scope / IP 白名单 / 防重放 / 限流 · 调用日志 · 增量同步 · Python / Node / Java SDK |
| **消息与任务** | 站内消息(实时推送)· 通知公告(富文本 + 附件)· 定时任务(在线调度 · cron 预览 · 执行记录) |
| **能力开放** | AK/SK 签名的开放接口 · scope / IP 白名单 / 防重放 / 限流 · 调用日志 · 增量同步 · Python / Node / Java SDK · Webhook 事件订阅(Standard Webhooks 签名 · 失败重试 · 投递记录 · 批量重发) |
| **消息与通知** | 站内消息(实时推送)· 通知公告(富文本 + 附件 · 发布范围 · 定时发布 · 置顶 / 级别 / 有效期 · 登录后弹窗与必须确认 · 顶部横幅 · 登录页公告 · 阅读统计)· 通知渠道路由(邮件 / 短信 / 飞书·钉钉·企业微信群机器人 · 按事件配策略 · 投递记录)· 短信(阿里云 / 腾讯云) |
| **定时任务** | 在线调度 · cron 预览 · 执行记录 |
| **运维** | 参数配置 · 数据字典 · 登录 / 操作日志(自动脱敏)· 服务器与 Redis 监控 · 发新版提示 |
| **多端** | Electron 桌面端(静默打印 · 本地硬件 · 自动更新)· Expo 原生移动 App(不是 WebView 套壳) |
| **多端** | Electron 桌面端(三平台安装包 · 静默打印 · 本地硬件 · 自动更新)· Expo 原生移动 App(不是 WebView 套壳,**预览版**,见下方说明) |
| **开发者** | 组件沙箱(每个组件可调、可抄)· 调色盘 · 由 OpenAPI 生成的类型安全前后端契约 |

## 技术栈
Expand All @@ -86,6 +87,14 @@
| **多端** | Electron 42 · Expo 57 / React Native 0.86 |
| **工程** | pnpm + turbo 单仓 · uv · ruff · pytest · Playwright · GitHub Actions |

## 移动端(预览版)

Expo App 在 1.0 里是**预览版**:真的能用,但不在 1.0 的兼容承诺范围内。

- **已有**:首页 · 公告 · 消息 · 用户 · 个人中心 · 服务器监控 · 时区 · 手机验证码登录 · 会话结束原因提示
- **还没有**:审批 · 角色 / 部门 / 菜单 · 文件 · 操作日志与登录日志(「应用」页明写「还没做」,不藏起来)· 在 App 里绑定手机号 / 开二次验证
- **还没验证**:没有自动化测试([#137](https://github.com/yilecoding/fastapi-react-admin/issues/137))· 多数页面没上过真机 · 没有发版流水线

## 数据库:三选一

改 `.env` 里的 `DATABASE_TYPE` 就能换,同一套代码、同一条迁移链,三种库各有一份初始数据。
Expand Down
2 changes: 1 addition & 1 deletion apps/api/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ def _register_model_globals() -> None:
# 产品版本号,/docs 顶上的徽章、AI 的 system_info 工具、OTel 的 service.version 都读它。
# 🔴 必须和 `apps/web/package.json` 的 version 同值 —— 打 tag 时 release.yml 两边一起校验,不一致第一个 job 就红。
# 原来写的是上游 FBA 的版本(1.15.1),/docs 上一直挂着一个和本产品无关的号;上游基线记在 `apps/api/.upstream-baseline`
__version__ = '0.13.0'
__version__ = '1.0.0'
3 changes: 2 additions & 1 deletion apps/api/backend/common/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ def process_bind_param(self, value: datetime | None, dialect) -> datetime | None
# 「已经是应用时区的墙钟时间」,只需要补时区标记,不需要换算。
value = value.replace(tzinfo=timezone.tz_info)
elif value is not None and value.utcoffset() != timezone.now().utcoffset():
# TODO 处理夏令时偏移
# 夏令时不用另外处理:astimezone 按「那个时刻」查规则,冬天的值夏天写入也还是同一时刻、
# 带当时的偏移。唯一缺口是 MySQL 秋季回拨那一小时(DATETIME 不存偏移),见 tests/test_timezone_dst.py
value = timezone.from_datetime(value)
return value

Expand Down
86 changes: 86 additions & 0 deletions apps/api/backend/tests/test_timezone_dst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""`TimeZone` 遇到夏令时(#200:model.py 里那句「TODO 处理夏令时偏移」到底要不要处理)

结论:**带时区的值不会被夏令时搞错**。那条分支是「偏移和*现在*不一样就 `astimezone(应用时区)`」——
冬天的值在夏天写入时偏移确实不同,但 `astimezone` 按**那个时刻**查规则,换算出来还是同一个时刻、
带着当时正确的偏移,没有平移一小时。所以 TODO 不需要「处理」,这里把它钉成测试。

唯一的缺口在 **MySQL**(`DATETIME` 不存偏移,只存应用时区的墙上时间):秋季回拨那一小时的墙上时间
出现两次,读回来只能当成第一次(`fold=0`),第二次的记录会早一小时。PostgreSQL / SQL Server
存的是带偏移的时刻,不受影响。见最后那条 xfail。

⚠️ 顺带实测到一个 Python 的坑:**同一个 ZoneInfo 的两个 aware datetime 相减是按墙上时间减的**,
跨夏令时差一小时。算时长要先 `.astimezone(UTC)`(PostgreSQL / SQL Server 驱动读回来本来就是
UTC 或固定偏移,不中招;MySQL 读回来被补上应用时区,会中招)。

应用时区刻意设成有夏令时的 `America/New_York`(默认的 `Asia/Shanghai` 没有夏令时,什么都测不出来)。
"""

from datetime import UTC, datetime, timedelta
from zoneinfo import ZoneInfo

import pytest

from backend.common.model import TimeZone
from backend.utils.timezone import timezone

NY = ZoneInfo('America/New_York')
SUMMER_NOW = datetime(2026, 7, 1, 12, 0, tzinfo=NY) # EDT,-04:00


@pytest.fixture
def ny(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(timezone, 'tz_info', NY)
monkeypatch.setattr(timezone, 'now', lambda: SUMMER_NOW)


def bind(value: datetime) -> datetime:
return TimeZone().process_bind_param(value, None)


def test_winter_value_written_in_summer_keeps_its_instant_and_offset(ny: None) -> None:
"""🔴 那条 TODO 分支:冬天(-05:00)的值、夏天(-04:00)写入,不能被平移一小时"""
winter = datetime(2026, 1, 15, 9, 0, tzinfo=NY)
out = bind(winter)
assert out == winter # 同一个时刻
assert out.utcoffset() == timedelta(hours=-5) # 偏移是当时的,不是「现在」的


def test_utc_value_is_converted_to_the_offset_of_that_date(ny: None) -> None:
out = bind(datetime(2026, 1, 15, 14, 0, tzinfo=UTC))
assert out == datetime(2026, 1, 15, 14, 0, tzinfo=UTC)
assert (out.hour, out.utcoffset()) == (9, timedelta(hours=-5))


@pytest.mark.parametrize(
('naive', 'offset'),
[
(datetime(2026, 1, 15, 9, 0, tzinfo=NY).replace(tzinfo=None), -5),
(datetime(2026, 7, 15, 9, 0, tzinfo=NY).replace(tzinfo=None), -4),
],
ids=['winter', 'summer'],
)
def test_naive_value_gets_the_offset_of_its_own_date(ny: None, naive: datetime, offset: int) -> None:
"""naive 值当作应用时区的墙上时间,偏移按它自己那天算"""
assert bind(naive).utcoffset() == timedelta(hours=offset)


def test_spring_forward_instants_round_trip(ny: None) -> None:
"""春季拨快前后各一分钟(01:59 EST → 03:00 EDT),时刻都不变"""
before = datetime(2026, 3, 8, 6, 59, tzinfo=UTC)
after = datetime(2026, 3, 8, 7, 0, tzinfo=UTC)
assert bind(before) == before
assert bind(after) == after
# ⚠️ 不能直接 `bind(after) - bind(before)`:同一个 ZoneInfo 的两个值相减,Python 按**墙上时间**减、
# 不看偏移,跨夏令时会算出 61 分钟(实测)。算时长先转 UTC
assert bind(after).astimezone(UTC) - bind(before).astimezone(UTC) == timedelta(minutes=1)


@pytest.mark.xfail(
strict=True,
reason='MySQL 的 DATETIME 不存偏移:秋季回拨那一小时读回来一律当第一次(fold=0),第二次会早一小时',
)
def test_mysql_fall_back_second_occurrence_round_trip(ny: None) -> None:
second = datetime(2026, 11, 1, 6, 30, tzinfo=UTC) # 01:30 EST,回拨后第二次的 01:30
wall = bind(second).replace(tzinfo=None) # MySQL 存的就是这个墙上时间
back = TimeZone().process_result_value(wall, None)
assert back == second
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api",
"version": "0.13.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "uv run python -m uvicorn backend.main:app --port 8088 --reload --reload-dir backend",
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/src/content/docs/en/reference/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ How you upgrade depends on how you use the project. If you **deploy the publishe

## How are versions numbered?

Semver: a big feature bumps the minor version (0.11 → 0.12), fixes only bump the patch (0.9.0 → 0.9.1). It is still 0.x, but the compatibility rules below are already in force.
Semver: breaking any of the compatibility rules below bumps the major version (1.x → 2.0), a big feature bumps the minor version (1.0 → 1.1), fixes only bump the patch (1.0.0 → 1.0.1). From 1.0.0 these rules are formally in force.

The mobile app is a **preview** in 1.0 and is not covered by these rules (see "Mobile app (preview)" in the README for what it has and lacks).

## What is promised not to break?

Expand Down
4 changes: 3 additions & 1 deletion apps/docs/src/content/docs/reference/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ sidebar:

## 版本号怎么定

按 semver:合进一个大功能升次版本号(0.11 → 0.12),只有修复升修订号(0.9.0 → 0.9.1)。现在还是 0.x,但已经按下面的兼容约定执行。
按 semver:破坏下面兼容约定里的任何一条升主版本号(1.x → 2.0),合进一个大功能升次版本号(1.0 → 1.1),只有修复升修订号(1.0.0 → 1.0.1)。1.0.0 起正式按这套约定执行。

移动端 App 在 1.0 里是**预览版**,不在兼容约定范围内(已有 / 还没有的功能见 README「移动端(预览版)」)。

## 哪些东西承诺不破坏

Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.13.0",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "0.13.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "cd ../api && CELERY_CUSTOM_WORKER_POOL=celery_aio_pool.pool:AsyncIOPool uv run --no-sync celery -A backend.app.task.celery worker -B -l info --pool=custom"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fastapi-react-admin",
"version": "0.13.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "turbo dev --filter=!@admin/desktop",
Expand Down
Loading