From 5a45ee1106a7baf2cb06c180fcac17c4d2f5b100 Mon Sep 17 00:00:00 2001 From: ChengCan <783785929@qq.com> Date: Wed, 25 Mar 2026 15:58:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=BB=BA=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=EF=BC=8C=E6=96=B0=E5=BB=BA=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mini-program/.env.example | 11 + mini-program/app/__init__.py | 1 + mini-program/app/main.py | 35 +++ mini-program/app/routers/__init__.py | 1 + mini-program/app/routers/health.py | 8 + mini-program/app/settings.py | 46 +++ mini-program/database/sql/001_init_chat.sql | 295 ++++++++++++++++++++ mini-program/requirements.txt | 4 + 8 files changed, 401 insertions(+) create mode 100644 mini-program/.env.example create mode 100644 mini-program/app/__init__.py create mode 100644 mini-program/app/main.py create mode 100644 mini-program/app/routers/__init__.py create mode 100644 mini-program/app/routers/health.py create mode 100644 mini-program/app/settings.py create mode 100644 mini-program/database/sql/001_init_chat.sql create mode 100644 mini-program/requirements.txt diff --git a/mini-program/.env.example b/mini-program/.env.example new file mode 100644 index 0000000..6202b6b --- /dev/null +++ b/mini-program/.env.example @@ -0,0 +1,11 @@ +APP_NAME=mini-program-api +APP_VERSION=0.1.0 +APP_DESCRIPTION=Backend service for mini program +HOST=0.0.0.0 +PORT=8001 + +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=change_me +DB_NAME=mini_program diff --git a/mini-program/app/__init__.py b/mini-program/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mini-program/app/__init__.py @@ -0,0 +1 @@ + diff --git a/mini-program/app/main.py b/mini-program/app/main.py new file mode 100644 index 0000000..60084ed --- /dev/null +++ b/mini-program/app/main.py @@ -0,0 +1,35 @@ +import os + +from fastapi import FastAPI + +try: + # For module mode: `uvicorn app.main:app` + from app.routers.health import router as health_router + from app.settings import settings +except ModuleNotFoundError: + # For script mode: `python app/main.py` or VS Code "Run Python File" + from routers.health import router as health_router + from settings import settings + + +def create_app() -> FastAPI: + app = FastAPI( + title=settings.app_name, + version=settings.app_version, + description=settings.app_description, + ) + app.include_router(health_router) + return app + + +app = create_app() + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + app, + host=os.getenv("HOST", settings.host), + port=int(os.getenv("PORT", str(settings.port))), + ) diff --git a/mini-program/app/routers/__init__.py b/mini-program/app/routers/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mini-program/app/routers/__init__.py @@ -0,0 +1 @@ + diff --git a/mini-program/app/routers/health.py b/mini-program/app/routers/health.py new file mode 100644 index 0000000..9eda72b --- /dev/null +++ b/mini-program/app/routers/health.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter(tags=["health"]) + + +@router.get("/health") +async def health() -> dict[str, str]: + return {"status": "ok"} diff --git a/mini-program/app/settings.py b/mini-program/app/settings.py new file mode 100644 index 0000000..dbb078b --- /dev/null +++ b/mini-program/app/settings.py @@ -0,0 +1,46 @@ +from functools import lru_cache +from urllib.parse import quote_plus + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + app_name: str = Field(default="mini-program-api", validation_alias="APP_NAME") + app_version: str = Field(default="0.1.0", validation_alias="APP_VERSION") + app_description: str = Field( + default="Backend service for mini program", + validation_alias="APP_DESCRIPTION", + ) + host: str = Field(default="0.0.0.0", validation_alias="HOST") + port: int = Field(default=8001, validation_alias="PORT") + + db_host: str = Field(default="127.0.0.1", validation_alias="DB_HOST") + db_port: int = Field(default=3306, validation_alias="DB_PORT") + db_user: str = Field(default="root", validation_alias="DB_USER") + db_password: str = Field(default="", validation_alias="DB_PASSWORD") + db_name: str = Field(default="mini_program", validation_alias="DB_NAME") + + @property + def mysql_dsn(self) -> str: + user = quote_plus(self.db_user) + password = quote_plus(self.db_password) + auth = f"{user}:{password}" if password else user + return ( + f"mysql+pymysql://{auth}@{self.db_host}:{self.db_port}/" + f"{self.db_name}?charset=utf8mb4" + ) + + +@lru_cache(maxsize=1) +def get_settings() -> Settings: + return Settings() + + +settings = get_settings() diff --git a/mini-program/database/sql/001_init_chat.sql b/mini-program/database/sql/001_init_chat.sql new file mode 100644 index 0000000..20a92d0 --- /dev/null +++ b/mini-program/database/sql/001_init_chat.sql @@ -0,0 +1,295 @@ +-- Chat schema and seed data for mini-program +-- Target: MySQL 8+ + +CREATE DATABASE IF NOT EXISTS mini_program + DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE mini_program; + +SET FOREIGN_KEY_CHECKS=0; +DROP TABLE IF EXISTS chat_message; +DROP TABLE IF EXISTS chat_conversation_member; +DROP TABLE IF EXISTS chat_conversation; +DROP TABLE IF EXISTS chat_user; +SET FOREIGN_KEY_CHECKS=1; + +CREATE TABLE IF NOT EXISTS chat_user ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + nickname VARCHAR(64) NULL, + avatar_file_key VARCHAR(255) NULL COMMENT 'Stored file key, not URL', + status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1-active 2-disabled 3-deleted', + last_login_at DATETIME(3) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + KEY idx_status (status), + KEY idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS chat_conversation ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + title VARCHAR(128) NULL, + conversation_type TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1-direct 2-group', + member_signature CHAR(64) NOT NULL COMMENT 'sha256 of sorted member user_ids, e.g. sha256("1,2")', + status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1-active 2-archived', + last_seq BIGINT UNSIGNED NOT NULL DEFAULT 0, + message_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + last_message_preview VARCHAR(255) NULL, + last_message_at DATETIME(3) NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE KEY uk_type_member_signature (conversation_type, member_signature), + KEY idx_last_message_at (last_message_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS chat_conversation_member ( + conversation_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + member_role TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT '1-owner 2-member', + joined_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + left_at DATETIME(3) NULL, + PRIMARY KEY (conversation_id, user_id), + KEY idx_user_conversation (user_id, conversation_id), + CONSTRAINT fk_member_conv + FOREIGN KEY (conversation_id) REFERENCES chat_conversation(id) + ON DELETE CASCADE, + CONSTRAINT fk_member_user + FOREIGN KEY (user_id) REFERENCES chat_user(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS chat_message ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + conversation_id BIGINT UNSIGNED NOT NULL, + seq BIGINT UNSIGNED NOT NULL COMMENT 'Conversation-local incremental sequence', + sender_user_id BIGINT UNSIGNED NULL COMMENT 'Null means assistant/system sender', + role TINYINT UNSIGNED NOT NULL COMMENT '1-user 2-assistant 3-system 4-tool', + content_type TINYINT UNSIGNED NOT NULL COMMENT '1-text 2-audio 3-image 4-json', + content_text MEDIUMTEXT NULL, + content_json JSON NULL, + media_file_key VARCHAR(255) NULL COMMENT 'Stored file key, not URL', + media_duration_ms INT UNSIGNED NULL, + client_msg_id VARCHAR(64) NULL COMMENT 'Idempotency key from client', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + deleted_at DATETIME(3) NULL, + PRIMARY KEY (id), + UNIQUE KEY uk_conv_seq (conversation_id, seq), + UNIQUE KEY uk_conv_client_msg (conversation_id, client_msg_id), + KEY idx_conv_created (conversation_id, created_at), + KEY idx_sender_created (sender_user_id, created_at), + CONSTRAINT fk_msg_conv + FOREIGN KEY (conversation_id) REFERENCES chat_conversation(id) + ON DELETE CASCADE, + CONSTRAINT fk_msg_sender_user + FOREIGN KEY (sender_user_id) REFERENCES chat_user(id) + ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT INTO chat_user ( + id, + nickname, + avatar_file_key, + status, + last_login_at, + created_at, + updated_at +) +VALUES + ( + 1, + 'Parent A', + 'avatars/parent-a.png', + 1, + '2026-03-25 09:58:00.000', + '2026-03-25 09:00:00.000', + '2026-03-25 09:58:00.000' + ), + ( + 2, + 'Parent B', + 'avatars/parent-b.png', + 1, + '2026-03-25 10:01:00.000', + '2026-03-25 09:20:00.000', + '2026-03-25 10:01:00.000' + ), + ( + 3, + 'Parent C', + 'avatars/parent-c.png', + 1, + '2026-03-25 21:11:00.000', + '2026-03-25 20:30:00.000', + '2026-03-25 21:11:00.000' + ); + +INSERT INTO chat_conversation ( + id, + title, + conversation_type, + member_signature, + status, + last_seq, + message_count, + last_message_preview, + last_message_at, + created_at, + updated_at +) +VALUES + ( + 1, + 'Direct Chat', + 1, + '17f8af97ad4a7f7639a4c9171d5185cbafb85462877a4746c21bdb0a4f940ca0', + 1, + 4, + 4, + 'user: Great, explain how to solve it.', + '2026-03-25 10:00:30.000', + '2026-03-25 10:00:00.000', + '2026-03-25 10:00:30.000' + ), + ( + 2, + 'Family Group', + 2, + '8a6ae15122001229edb8866f56e342af12ae8187203c3e3b33931743e7c0c48d', + 1, + 3, + 3, + 'user: Okay, we will wait in the living room.', + '2026-03-25 21:16:10.000', + '2026-03-25 21:15:00.000', + '2026-03-25 21:16:10.000' + ); + +INSERT INTO chat_conversation_member ( + conversation_id, + user_id, + member_role, + joined_at, + left_at +) +VALUES + (1, 1, 1, '2026-03-25 10:00:00.000', NULL), + (1, 2, 2, '2026-03-25 10:00:00.000', NULL), + (2, 1, 1, '2026-03-25 21:15:00.000', NULL), + (2, 2, 2, '2026-03-25 21:15:00.000', NULL), + (2, 3, 2, '2026-03-25 21:15:00.000', NULL); + +INSERT INTO chat_message ( + id, + conversation_id, + seq, + sender_user_id, + role, + content_type, + content_text, + content_json, + media_file_key, + media_duration_ms, + client_msg_id, + created_at +) +VALUES + ( + 1001, + 1, + 1, + 1, + 1, + 1, + 'Hi, did you finish homework?', + JSON_OBJECT('lang', 'en'), + NULL, + NULL, + 'c1-m1', + '2026-03-25 10:00:05.000' + ), + ( + 1002, + 1, + 2, + 2, + 1, + 1, + 'Almost done, one question left.', + JSON_OBJECT('lang', 'en'), + NULL, + NULL, + 'c1-m2', + '2026-03-25 10:00:12.000' + ), + ( + 1003, + 1, + 3, + NULL, + 2, + 1, + 'I can help explain the last question.', + JSON_OBJECT('tone', 'assistant'), + NULL, + NULL, + 'c1-m3', + '2026-03-25 10:00:20.000' + ), + ( + 1004, + 1, + 4, + 1, + 1, + 1, + 'Great, explain how to solve it.', + JSON_OBJECT('lang', 'en'), + NULL, + NULL, + 'c1-m4', + '2026-03-25 10:00:30.000' + ), + ( + 2001, + 2, + 1, + 1, + 1, + 1, + 'Dinner at 7?', + JSON_OBJECT('lang', 'en'), + NULL, + NULL, + 'c2-m1', + '2026-03-25 21:15:08.000' + ), + ( + 2002, + 2, + 2, + 3, + 1, + 2, + NULL, + JSON_OBJECT('note', 'voice message'), + 'audios/family-group-001.mp3', + 95000, + 'c2-m2', + '2026-03-25 21:15:30.000' + ), + ( + 2003, + 2, + 3, + 2, + 1, + 1, + 'Okay, we will wait in the living room.', + JSON_OBJECT('lang', 'en'), + NULL, + NULL, + 'c2-m3', + '2026-03-25 21:16:10.000' + ); diff --git a/mini-program/requirements.txt b/mini-program/requirements.txt new file mode 100644 index 0000000..3f5d45a --- /dev/null +++ b/mini-program/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.11 +uvicorn[standard]==0.34.0 +pydantic-settings==2.8.1 +python-dotenv==1.0.1