diff --git a/backend/app/main.py b/backend/app/main.py index a2ef0dcf..d0d46837 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -2,6 +2,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text from app.api.v1.router import api_router from app.core.config import settings @@ -18,12 +19,46 @@ async def lifespan(_: FastAPI): if settings.ENV == "development": async with engine.begin() as conn: + await _ensure_legacy_primary_keys(conn) await conn.run_sync(Base.metadata.create_all) async with SessionLocal() as session: await ensure_admin_exists(session) yield +async def _ensure_legacy_primary_keys(conn) -> None: + # Fix legacy databases where tables with an "id" column were created without a PK. + result = await conn.execute( + text( + """ + SELECT c.relname AS table_name + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'public' + AND c.relkind = 'r' + AND EXISTS ( + SELECT 1 + FROM pg_attribute a + WHERE a.attrelid = c.oid + AND a.attname = 'id' + AND a.attisdropped = false + ) + AND NOT EXISTS ( + SELECT 1 + FROM pg_constraint con + WHERE con.conrelid = c.oid + AND con.contype = 'p' + ) + """ + ) + ) + tables = [row.table_name for row in result.fetchall()] + for table_name in tables: + await conn.execute( + text(f'ALTER TABLE "{table_name}" ADD CONSTRAINT "{table_name}_pkey" PRIMARY KEY (id)') + ) + + def create_app() -> FastAPI: app = FastAPI( title="CTMS 后端 API", diff --git a/database/init.sql b/database/init.sql index d81e0c2e..5bd95a78 100644 --- a/database/init.sql +++ b/database/init.sql @@ -20,6 +20,35 @@ SET default_tablespace = ''; SET default_table_access_method = heap; +-- +-- Name: user_role; Type: TYPE; Schema: public; Owner: ctms_user +-- + +CREATE TYPE public.user_role AS ENUM ( + 'ADMIN', + 'PM', + 'CRA', + 'PV', + 'IMP' +); + + +ALTER TYPE public.user_role OWNER TO ctms_user; + +-- +-- Name: user_status; Type: TYPE; Schema: public; Owner: ctms_user +-- + +CREATE TYPE public.user_status AS ENUM ( + 'PENDING', + 'ACTIVE', + 'REJECTED', + 'DISABLED' +); + + +ALTER TYPE public.user_status OWNER TO ctms_user; + -- -- Name: adverse_events; Type: TABLE; Schema: public; Owner: ctms_user -- @@ -428,36 +457,23 @@ CREATE TABLE public.subjects ( ALTER TABLE public.subjects OWNER TO ctms_user; -- --- - - id uuid NOT NULL, - study_id uuid NOT NULL, - milestone_id uuid, - title character varying(200) NOT NULL, - description text, - assignee_id uuid, - priority character varying(20) NOT NULL, - due_date date, - status character varying(20) NOT NULL, - completed_at timestamp with time zone, - created_by uuid NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL -); - - - -- -- Name: users; Type: TABLE; Schema: public; Owner: ctms_user -- CREATE TABLE public.users ( id uuid NOT NULL, - username character varying(50) NOT NULL, - hashed_password character varying(255) NOT NULL, - role character varying(20) NOT NULL, - is_active boolean DEFAULT true NOT NULL, - created_at timestamp with time zone DEFAULT now() NOT NULL + email character varying(255) NOT NULL, + password_hash character varying(255) NOT NULL, + full_name character varying(255) NOT NULL, + role public.user_role NOT NULL, + department character varying(255) NOT NULL, + status public.user_status DEFAULT 'PENDING'::public.user_status NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + approved_by uuid, + approved_at timestamp with time zone, + avatar_url character varying(500) ); @@ -677,21 +693,14 @@ COPY public.subjects (id, study_id, site_id, subject_no, status, screening_date, \. --- --- - -77777777-7777-7777-7777-777777777777 44444444-4444-4444-4444-444444444444 66666666-6666-6666-6666-666666666666 伦理资料提交 准备伦理文件并提交 33333333-3333-3333-3333-333333333333 HIGH 2025-01-20 DOING \N 22222222-2222-2222-2222-222222222222 2025-12-18 00:55:17.568504+00 2025-12-18 00:55:17.568504+00 -\. - - -- -- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: ctms_user -- -COPY public.users (id, username, hashed_password, role, is_active, created_at) FROM stdin; -11111111-1111-1111-1111-111111111111 admin@example.com $2b$12$wvADh3A4snBRZH9/24KWbO22h6mgE3TNquzD6cy0Zu7kKKnCouwzW ADMIN t 2025-12-18 00:55:17.568504+00 -22222222-2222-2222-2222-222222222222 pm@example.com $2b$12$qbIguZoGxPDhczU98iWKE.01lpBFnrrw5kbcQdwLLw.oduq9feGcu PM t 2025-12-18 00:55:17.568504+00 -33333333-3333-3333-3333-333333333333 cra@example.com $2b$12$3VtakMu1oJXLMOgu0ylNj.F6iqmIkRgP1XI51hzoIMwHccjeMW2nO CRA t 2025-12-18 00:55:17.568504+00 +COPY public.users (id, email, password_hash, full_name, role, department, status, created_at, updated_at, approved_by, approved_at, avatar_url) FROM stdin; +11111111-1111-1111-1111-111111111111 admin@example.com $2b$12$wvADh3A4snBRZH9/24KWbO22h6mgE3TNquzD6cy0Zu7kKKnCouwzW System Admin ADMIN SYSTEM ACTIVE 2025-12-18 00:55:17.568504+00 2025-12-18 00:55:17.568504+00 \N \N \N +22222222-2222-2222-2222-222222222222 pm@example.com $2b$12$qbIguZoGxPDhczU98iWKE.01lpBFnrrw5kbcQdwLLw.oduq9feGcu Project Manager PM PMO ACTIVE 2025-12-18 00:55:17.568504+00 2025-12-18 00:55:17.568504+00 \N \N \N +33333333-3333-3333-3333-333333333333 cra@example.com $2b$12$3VtakMu1oJXLMOgu0ylNj.F6iqmIkRgP1XI51hzoIMwHccjeMW2nO CRA User CRA CRA ACTIVE 2025-12-18 00:55:17.568504+00 2025-12-18 00:55:17.568504+00 \N \N \N \. @@ -1193,10 +1202,10 @@ CREATE INDEX ix_subjects_study_id ON public.subjects USING btree (study_id); -- --- Name: ix_users_username; Type: INDEX; Schema: public; Owner: ctms_user +-- Name: ix_users_email; Type: INDEX; Schema: public; Owner: ctms_user -- -CREATE UNIQUE INDEX ix_users_username ON public.users USING btree (username); +CREATE UNIQUE INDEX ix_users_email ON public.users USING btree (email); --