From 84874707f58cca7aba6e54e314b656a7dedde64f Mon Sep 17 00:00:00 2001 From: shumengya Date: Sat, 21 Mar 2026 20:22:00 +0800 Subject: [PATCH] feat: major update - MySQL, chat, wishlist, PWA, admin overhaul --- .gitignore | 4 + API_DOCS.md | 543 ++ mengyastore-backend/README.md | 256 + mengyastore-backend/cmd/migrate/main.go | 304 + mengyastore-backend/data/json/orders.json | 119 - mengyastore-backend/data/json/products.json | 181 - mengyastore-backend/data/json/site.json | 3 - mengyastore-backend/docker-compose.yml | 5 +- mengyastore-backend/go.mod | 10 +- mengyastore-backend/go.sum | 14 + .../internal/auth/sproutgate.go | 2 + mengyastore-backend/internal/config/config.go | 17 + mengyastore-backend/internal/database/db.go | 45 + .../internal/database/models.go | 121 + mengyastore-backend/internal/email/email.go | 192 + .../internal/handlers/admin.go | 194 +- .../internal/handlers/admin_chat.go | 88 + .../internal/handlers/admin_orders.go | 35 + .../internal/handlers/admin_product.go | 202 + .../internal/handlers/admin_site.go | 73 + mengyastore-backend/internal/handlers/chat.go | 82 + .../internal/handlers/order.go | 171 +- .../internal/handlers/stats.go | 14 + .../internal/handlers/wishlist.go | 88 + mengyastore-backend/internal/models/chat.go | 12 + mengyastore-backend/internal/models/order.go | 5 + .../internal/models/product.go | 6 + .../internal/storage/chatstore.go | 99 + .../internal/storage/jsonstore.go | 338 +- .../internal/storage/orderstore.go | 211 +- .../internal/storage/sitestore.go | 199 +- .../internal/storage/wishliststore.go | 38 + mengyastore-backend/main.go | 47 +- mengyastore-backend/mengyastore-backend.exe | Bin 13952512 -> 0 bytes mengyastore-backend/mengyastore-backend.exe~ | Bin 13947904 -> 0 bytes mengyastore-frontend/README.md | 167 + mengyastore-frontend/index.html | 20 +- mengyastore-frontend/package-lock.json | 5529 ++++++++++++++++- mengyastore-frontend/package.json | 4 +- .../public/apple-touch-icon-180x180.png | Bin 0 -> 1389 bytes mengyastore-frontend/public/favicon.ico | Bin 0 -> 2083 bytes mengyastore-frontend/public/icon.svg | 18 + .../public/maskable-icon-512x512.png | Bin 0 -> 4030 bytes mengyastore-frontend/public/pwa-192x192.png | Bin 0 -> 4949 bytes mengyastore-frontend/public/pwa-512x512.png | Bin 0 -> 43121 bytes mengyastore-frontend/public/pwa-64x64.png | Bin 0 -> 1497 bytes mengyastore-frontend/src/App.vue | 54 +- mengyastore-frontend/src/assets/styles.css | 268 +- .../src/modules/admin/AdminPage.vue | 1059 ++-- .../admin/components/AdminChatPanel.vue | 605 ++ .../admin/components/AdminMaintenanceRow.vue | 155 + .../admin/components/AdminOrderTable.vue | 404 ++ .../admin/components/AdminProductModal.vue | 431 ++ .../admin/components/AdminProductTable.vue | 317 + .../modules/admin/components/AdminSMTPRow.vue | 182 + .../admin/components/AdminTokenRow.vue | 103 + .../src/modules/auth/AuthCallback.vue | 42 +- .../src/modules/chat/ChatWidget.vue | 545 ++ .../modules/maintenance/MaintenancePage.vue | 93 + .../src/modules/shared/SplashScreen.vue | 194 + .../src/modules/shared/api.js | 96 + .../src/modules/shared/auth.js | 32 +- .../src/modules/shared/useWishlist.js | 66 + .../src/modules/store/CheckoutPage.vue | 163 +- .../src/modules/store/ProductDetail.vue | 35 +- .../src/modules/store/StorePage.vue | 381 +- .../modules/store/components/ProductCard.vue | 342 + .../src/modules/user/MyOrdersPage.vue | 277 +- .../src/modules/wishlist/WishlistPage.vue | 108 + mengyastore-frontend/src/router/index.js | 29 +- mengyastore-frontend/vite.config.js | 51 +- 71 files changed, 13457 insertions(+), 2031 deletions(-) create mode 100644 API_DOCS.md create mode 100644 mengyastore-backend/README.md create mode 100644 mengyastore-backend/cmd/migrate/main.go delete mode 100644 mengyastore-backend/data/json/orders.json delete mode 100644 mengyastore-backend/data/json/products.json delete mode 100644 mengyastore-backend/data/json/site.json create mode 100644 mengyastore-backend/internal/database/db.go create mode 100644 mengyastore-backend/internal/database/models.go create mode 100644 mengyastore-backend/internal/email/email.go create mode 100644 mengyastore-backend/internal/handlers/admin_chat.go create mode 100644 mengyastore-backend/internal/handlers/admin_orders.go create mode 100644 mengyastore-backend/internal/handlers/admin_product.go create mode 100644 mengyastore-backend/internal/handlers/admin_site.go create mode 100644 mengyastore-backend/internal/handlers/chat.go create mode 100644 mengyastore-backend/internal/handlers/wishlist.go create mode 100644 mengyastore-backend/internal/models/chat.go create mode 100644 mengyastore-backend/internal/storage/chatstore.go create mode 100644 mengyastore-backend/internal/storage/wishliststore.go delete mode 100644 mengyastore-backend/mengyastore-backend.exe delete mode 100644 mengyastore-backend/mengyastore-backend.exe~ create mode 100644 mengyastore-frontend/README.md create mode 100644 mengyastore-frontend/public/apple-touch-icon-180x180.png create mode 100644 mengyastore-frontend/public/favicon.ico create mode 100644 mengyastore-frontend/public/icon.svg create mode 100644 mengyastore-frontend/public/maskable-icon-512x512.png create mode 100644 mengyastore-frontend/public/pwa-192x192.png create mode 100644 mengyastore-frontend/public/pwa-512x512.png create mode 100644 mengyastore-frontend/public/pwa-64x64.png create mode 100644 mengyastore-frontend/src/modules/admin/components/AdminChatPanel.vue create mode 100644 mengyastore-frontend/src/modules/admin/components/AdminMaintenanceRow.vue create mode 100644 mengyastore-frontend/src/modules/admin/components/AdminOrderTable.vue create mode 100644 mengyastore-frontend/src/modules/admin/components/AdminProductModal.vue create mode 100644 mengyastore-frontend/src/modules/admin/components/AdminProductTable.vue create mode 100644 mengyastore-frontend/src/modules/admin/components/AdminSMTPRow.vue create mode 100644 mengyastore-frontend/src/modules/admin/components/AdminTokenRow.vue create mode 100644 mengyastore-frontend/src/modules/chat/ChatWidget.vue create mode 100644 mengyastore-frontend/src/modules/maintenance/MaintenancePage.vue create mode 100644 mengyastore-frontend/src/modules/shared/SplashScreen.vue create mode 100644 mengyastore-frontend/src/modules/shared/useWishlist.js create mode 100644 mengyastore-frontend/src/modules/store/components/ProductCard.vue create mode 100644 mengyastore-frontend/src/modules/wishlist/WishlistPage.vue diff --git a/.gitignore b/.gitignore index 8b12b4b..6d94488 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ coverage/ *.exe~ *~ .DS_Store +.cursor/ +config.json +mengyastore-backend/data/ +mengyastore-frontend/public/logo.png diff --git a/API_DOCS.md b/API_DOCS.md new file mode 100644 index 0000000..8dbd354 --- /dev/null +++ b/API_DOCS.md @@ -0,0 +1,543 @@ +# 萌芽账户认证中心 API 文档 + +访问 **`GET /`** 或 **`GET /api`**(无鉴权)可得到 JSON 格式的简要说明(服务名、版本、`/api/docs` 与 `/api/health` 入口、路由前缀摘要)。 + +接入地址: +- 统一登录前端:`https://auth.shumengya.top` +- 后端 API:`https://auth.api.shumengya.top` +- 本地开发 API:`http://:8080` + +对外接入建议: +1. 第三方应用按钮跳转到统一登录前端。 +2. 登录成功后回跳到业务站点。 +3. 业务站点使用回跳带回的 `token` 调用后端 API。 + +示例按钮: +```html + + 使用萌芽统一账户认证登录 + +``` + +回跳说明: +- 用户已登录时,统一登录前端会提示“继续授权”或“切换账号”。 +- 登录成功后会回跳到 `redirect_uri`(或 `return_url`),并在 URL **`#fragment`**(哈希)中带上令牌与用户信息(见下表)。 +- 第三方应用拿到 `token` 后,建议调用 **`POST /api/auth/verify`**(无副作用、适合网关鉴权)或 **`GET /api/auth/me`**(会更新访问记录,适合业务拉全量资料)校验并解析用户身份。 + +### 统一登录前端:查询参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `redirect_uri` | 与 `return_url` 至少其一 | 登录成功后的回跳地址,须进行 URL 编码;可为绝对 URL 或相对路径(相对路径相对统一登录站点解析)。 | +| `return_url` | 同上 | 与 `redirect_uri` 同义,二者都传时优先 `redirect_uri`。 | +| `state` | 否 | OAuth 风格透传字符串;回跳时原样写入哈希参数,供业务防 CSRF 或关联会话。 | +| `prompt` | 否 | 预留;前端可读,当前可用于将来扩展交互策略。 | +| `client_id` | 否 | 第三方应用稳定标识(字母数字开头,可含 `_.:-`,最长 64)。写入用户「应用接入记录」,并随登录请求提交给后端。 | +| `client_name` | 否 | 展示用名称(最长 128),与 `client_id` 配对;可选。 | + +### 回跳 URL:`#` 哈希参数 + +成功授权后,前端将使用 [`URLSearchParams`](https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams) 写入哈希,例如:`https://app.example.com/auth/callback#token=...&expiresAt=...&account=...&username=...&state=...`。 + +| 参数 | 说明 | +|------|------| +| `token` | JWT,调用受保护接口时放在请求头 `Authorization: Bearer `。 | +| `expiresAt` | 过期时间,RFC3339(与签发侧一致,当前默认为登录时起算 **7 天**)。 | +| `account` | 账户名(与 JWT `sub` 一致)。 | +| `username` | 展示用昵称,可能为空。 | +| `state` | 若登录请求携带了 `state`,则原样返回。 | + +业务站点回调页应用脚本读取 `location.hash`,解析后**仅在 HTTPS 环境**将 `token` 存于内存或安全存储,并尽快用后端 **`POST /api/auth/verify`** 校验(勿仅信任哈希中的明文字段)。 + +### 第三方后端接入建议 + +1. **仅信服务端**:回调页将 `token` 交给自有后端,由后端请求 `POST https:///api/auth/verify`(JSON body:`{"token":"..."}`),根据 `valid` 与 `user.account` 建立会话。 +2. **CORS**:浏览器直连 API 时须后端已配置 CORS(本服务默认允许任意 `Origin`);若从服务端发起请求则不受 CORS 限制。 +3. **令牌过期**:`verify` / `me` 返回 401 或 `verify` 中 `valid:false` 时,应引导用户重新走统一登录。 + +## 认证与统一登录 + +### 登录获取统一令牌 +`POST /api/auth/login` + +请求: +```json +{ + "account": "demo", + "password": "demo123", + "clientId": "my-app", + "clientName": "我的应用" +} +``` + +`clientId` / `clientName` 可选;规则与请求头 `X-Auth-Client` / `X-Auth-Client-Name` 一致。传入且格式合法时,会在登录成功后写入该用户的 **应用接入记录**(见下文 `authClients`)。 + +响应: +```json +{ + "token": "jwt-token", + "expiresAt": "2026-03-14T12:00:00Z", + "user": { + "account": "demo", + "username": "示例用户", + "email": "demo@example.com", + "level": 0, + "sproutCoins": 10, + "secondaryEmails": ["demo2@example.com"], + "phone": "13800000000", + "avatarUrl": "https://example.com/avatar.png", + "websiteUrl": "https://example.com", + "bio": "### 简介", + "createdAt": "2026-03-14T12:00:00Z", + "updatedAt": "2026-03-14T12:00:00Z" + } +} +``` + +若账户已被管理员封禁,返回 **403**,且**不会签发 JWT**,响应示例: + +```json +{ + "error": "account is banned", + "banReason": "违规内容" +} +``` + +`banReason` 可能为空字符串或省略。 + +**常见 HTTP 状态码(登录)** + +| 状态码 | 含义 | +|--------|------| +| 200 | 成功,返回 `token`、`expiresAt`、`user`。 | +| 400 | 请求体非法或缺少 `account` / `password`。 | +| 401 | 账户不存在或密码错误(统一文案 `invalid credentials`)。 | +| 403 | 账户已封禁(见上文 JSON)。 | +| 500 | 服务器内部错误(读库、签发 JWT 失败等)。 | + +**JWT 概要**:算法 **HS256**;载荷含 `account`(与 `sub` 一致)、`iss`(见 `data/config/auth.json`)、`iat` / `exp`。客户端只需透传字符串,**勿在前端解析密钥**。 + +### 校验令牌 +`POST /api/auth/verify` + +请求: +```json +{ + "token": "jwt-token" +} +``` + +响应: +```json +{ + "valid": true, + "user": { "account": "demo", "...": "..." } +} +``` + +若账户已封禁,返回 **200** 且 `valid` 为 **false**(不返回 `user` 对象),示例: + +```json +{ + "valid": false, + "error": "account is banned", + "banReason": "违规内容" +} +``` + +令牌过期、签名错误、issuer 不匹配等解析失败时返回 **401**,示例:`{"valid": false, "error": "invalid token"}`。 + +`verify` 与 `me` 的取舍:**仅校验身份、不改变用户数据**时用 `verify`;需要最新资料、签到状态或写入「最后访问」时用 `GET /api/auth/me`(需 Bearer)。 + +**应用接入记录(可选)**:第三方在 **`POST /api/auth/verify`** 或 **`GET /api/auth/me`** 上携带请求头: + +- `X-Auth-Client`:应用 ID(格式同登录 JSON 的 `clientId`) +- `X-Auth-Client-Name`:可选展示名 + +校验成功且用户未封禁时,服务端会更新该用户 JSON 中的 `authClients` 数组(`clientId`、`displayName`、`firstSeenAt`、`lastSeenAt`)。**`POST /api/auth/verify` 的响应体 `user` 仍为 `Public()`,不含 `authClients`**,避免向调用方泄露用户在其他应用的接入情况;**`GET /api/auth/me`** 与管理员列表中的 `user`(`OwnerPublic`)**包含** `authClients`,用户可在统一登录前端的个人中心查看。 + +### 获取当前用户信息 +`GET /api/auth/me` + +请求头: +`Authorization: Bearer ` + +可选(由前端调用 `https://cf-ip-geo.smyhub.com/api` 等接口解析后传入,用于记录「最后访问 IP」与「最后显示位置」): +- `X-Visit-Ip`:客户端公网 IP(与地理接口返回的 `ip` 一致即可) +- `X-Visit-Location`:展示用位置文案(例如将 `geo.countryName`、`regionName`、`cityName` 拼接为 `中国 四川 成都`) + +**服务端回退(避免浏览器跨域导致头缺失)**:若未传 `X-Visit-Location`,后端会用 `X-Visit-Ip`;若也未传 `X-Visit-Ip`,则用连接的 `ClientIP()`(请在前置反向代理上正确传递 `X-Forwarded-For` 等,并在生产环境为 Gin 配置可信代理)。随后服务端请求 `GEO_LOOKUP_URL`(默认 `https://cf-ip-geo.smyhub.com/api?ip=`)解析展示位置并写入用户记录。 + +响应: +```json +{ + "user": { "account": "demo", "...": "..." }, + "checkIn": { + "rewardCoins": 1, + "checkedInToday": false, + "lastCheckInDate": "", + "lastCheckInAt": "", + "today": "2026-03-14" + } +} +``` + +> `user` 还会包含 `lastVisitAt`、`lastVisitDate`、`checkInDays`、`checkInStreak`、`visitDays`、`visitStreak` 等统计字段。 + +> 在登录用户本人、管理员列表等场景下,`user` 还可包含 `lastVisitIp`、`lastVisitDisplayLocation`(最近一次通过 `/api/auth/me` 上报的访问 IP 与位置文案)。**公开用户资料接口** `GET /api/public/users/:account` 与 **`POST /api/auth/verify` 的 `user` 中不包含这两项**(避免公开展示或第三方校验时令牌响应携带访问隐私)。 + +> 说明:密码不会返回。 + +若账户在登录后被封禁,持旧 JWT 调用 `GET /api/auth/me`、`PUT /api/auth/profile`、`POST /api/auth/check-in`、辅助邮箱等需登录接口时,返回 **403**,正文同登录封禁响应(`error` + 可选 `banReason`)。客户端应作废本地令牌。 + +### 每日签到 +`POST /api/auth/check-in` + +请求头: +`Authorization: Bearer ` + +响应: +```json +{ + "checkedIn": true, + "alreadyCheckedIn": false, + "rewardCoins": 1, + "awardedCoins": 1, + "message": "签到成功", + "user": { "account": "demo", "...": "..." } +} +``` + +### 更新当前用户资料 +`PUT /api/auth/profile` + +请求头: +`Authorization: Bearer ` + +请求(字段可选): +```json +{ + "password": "newpass", + "username": "新昵称", + "phone": "13800000000", + "avatarUrl": "https://example.com/avatar.png", + "websiteUrl": "https://example.com", + "bio": "### 新简介" +} +``` + +说明:`websiteUrl` 须为 `http`/`https` 地址;可传空字符串清除;未写协议时服务端会补全为 `https://`。 + +响应: +```json +{ + "user": { "account": "demo", "...": "..." } +} +``` + +## 用户广场 + +### 获取用户公开主页 +`GET /api/public/users/{account}` + +说明: +- 仅支持账户名 `account`,不支持昵称查询。 +- 适合第三方应用展示用户公开资料。 +- 若该账户已被封禁,返回 **404** `{"error":"user not found"}`(与不存在账户相同,避免公开资料泄露)。 +- 响应中含该用户**最近一次被服务端记录的**访问 IP(`lastVisitIp`)与展示用地理位置(`lastVisitDisplayLocation`,与本人中心一致);`POST /api/auth/verify` 返回的用户 JSON **不含**上述两项。 + +响应: +```json +{ + "user": { + "account": "demo", + "username": "示例用户", + "level": 3, + "sproutCoins": 10, + "avatarUrl": "https://example.com/avatar.png", + "websiteUrl": "https://example.com", + "lastVisitIp": "203.0.113.1", + "lastVisitDisplayLocation": "中国 广东省 深圳市", + "bio": "### 简介" + } +} +``` + +### 公开注册策略 +`GET /api/public/registration-policy` + +无需鉴权。用于前端判断是否展示「邀请码」输入框。 + +响应: +```json +{ + "requireInviteCode": false +} +``` + +当 `requireInviteCode` 为 **true** 时,`POST /api/auth/register` 必须携带有效 `inviteCode`(见下节)。 + +### 注册账号(发送邮箱验证码) +`POST /api/auth/register` + +请求: +```json +{ + "account": "demo", + "password": "demo123", + "username": "示例用户", + "email": "demo@example.com", + "inviteCode": "ABCD1234" +} +``` + +- `inviteCode`:可选。若服务端开启「强制邀请码」,则必填且须为管理员发放的未过期、未用尽邀请码。邀请码**不区分大小写**;成功完成 `verify-email` 创建用户后才会扣减使用次数。 + +响应: +```json +{ + "sent": true, + "expiresAt": "2026-03-14T12:10:00Z" +} +``` + +### 验证邮箱并完成注册 +`POST /api/auth/verify-email` + +请求: +```json +{ + "account": "demo", + "code": "123456" +} +``` + +响应: +```json +{ + "created": true, + "user": { "account": "demo", "...": "..." } +} +``` + +### 忘记密码(发送重置验证码) +`POST /api/auth/forgot-password` + +请求: +```json +{ + "account": "demo", + "email": "demo@example.com" +} +``` + +响应: +```json +{ + "sent": true, + "expiresAt": "2026-03-14T12:10:00Z" +} +``` + +### 重置密码 +`POST /api/auth/reset-password` + +请求: +```json +{ + "account": "demo", + "code": "123456", + "newPassword": "newpass" +} +``` + +响应: +```json +{ "reset": true } +``` + +### 申请添加辅助邮箱(发送验证码) +`POST /api/auth/secondary-email/request` + +请求头: +`Authorization: Bearer ` + +请求: +```json +{ + "email": "demo2@example.com" +} +``` + +响应: +```json +{ + "sent": true, + "expiresAt": "2026-03-14T12:10:00Z" +} +``` + +### 验证辅助邮箱 +`POST /api/auth/secondary-email/verify` + +请求头: +`Authorization: Bearer ` + +请求: +```json +{ + "email": "demo2@example.com", + "code": "123456" +} +``` + +响应: +```json +{ + "verified": true, + "user": { "account": "demo", "...": "..." } +} +``` + +## 管理端接口(需要管理员 Token) + +管理员 Token 存放在 `data/config/admin.json` 中;如果文件不存在,后端启动时会自动生成并写入该文件。 +请求时可使用以下任一方式携带: +- Query:`?token=` +- Header:`X-Admin-Token: ` + +### 签到奖励设置 +`GET /api/admin/check-in/config` + +`PUT /api/admin/check-in/config` + +请求: +```json +{ + "rewardCoins": 1 +} +``` +- Header:`Authorization: Bearer ` + +### 注册策略与邀请码 + +`GET /api/admin/registration` + +响应含 `requireInviteCode` 与 `invites` 数组(每项含 `code`、`note`、`maxUses`、`uses`、`expiresAt`、`createdAt`)。`maxUses` 为 0 表示不限次数。 + +`PUT /api/admin/registration` + +请求: +```json +{ "requireInviteCode": true } +``` + +`POST /api/admin/registration/invites` + +请求: +```json +{ + "note": "内测批次", + "maxUses": 10, + "expiresAt": "2026-12-31T15:59:59Z" +} +``` + +`expiresAt` 可省略;须为 RFC3339。响应 `201`,`invite` 内含服务端生成的 8 位邀请码。 + +`DELETE /api/admin/registration/invites/{code}` + +删除指定邀请码(`code` 与存储大小写可能不同,按不区分大小写匹配)。 + +### 获取用户列表 +`GET /api/admin/users` + +响应: +```json +{ + "total": 1, + "users": [{ "account": "demo", "...": "..." }] +} +``` + +### 新建用户 +`POST /api/admin/users` + +请求: +```json +{ + "account": "demo", + "password": "demo123", + "username": "示例用户", + "email": "demo@example.com", + "level": 0, + "sproutCoins": 10, + "secondaryEmails": ["demo2@example.com"], + "phone": "13800000000", + "avatarUrl": "https://example.com/avatar.png", + "websiteUrl": "https://example.com", + "bio": "### 简介" +} +``` + +### 更新用户 +`PUT /api/admin/users/{account}` + +请求(字段可选): +```json +{ + "password": "newpass", + "username": "新昵称", + "level": 1, + "secondaryEmails": ["demo2@example.com"], + "sproutCoins": 99, + "websiteUrl": "https://example.com", + "banned": true, + "banReason": "违规说明(最多 500 字)" +} +``` + +- `banned`:是否封禁;解封时请传 `false`,并可将 `banReason` 置为空字符串。 +- `banReason`:仅当用户处于封禁状态时允许设为非空;封禁时若首次写入会记录 `bannedAt`(RFC3339,存于用户 JSON)。 + +管理员列表 `GET /api/admin/users` 中每条 `user` 可含 `banned`、`banReason`(不含 `bannedAt` 亦可从存储文件中查看)。 + +### 删除用户 +`DELETE /api/admin/users/{account}` + +响应: +```json +{ "deleted": true } +``` + +## 数据存储说明 + +- 用户数据:`data/users/*.json` +- 注册待验证:`data/pending/*.json` +- 密码重置记录:`data/reset/*.json` +- 辅助邮箱验证:`data/secondary/*.json` +- 管理员 Token:`data/config/admin.json` +- JWT 配置:`data/config/auth.json` +- 邮件配置:`data/config/email.json` +- 注册策略与邀请码:`data/config/registration.json` + +## 快速联调用示例 + +```bash +# 服务根路径 JSON 说明 +curl -s http://localhost:8080/ | jq . + +# 登录 +curl -X POST http://localhost:8080/api/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"account":"demo","password":"demo123"}' + +# 校验令牌(推荐第三方网关先调此接口) +curl -X POST http://localhost:8080/api/auth/verify \ + -H 'Content-Type: application/json' \ + -d '{"token":""}' + +# 使用令牌获取用户信息(会更新访问记录) +curl http://localhost:8080/api/auth/me \ + -H 'Authorization: Bearer ' +``` diff --git a/mengyastore-backend/README.md b/mengyastore-backend/README.md new file mode 100644 index 0000000..f2d615f --- /dev/null +++ b/mengyastore-backend/README.md @@ -0,0 +1,256 @@ +# 萌芽小店 · 后端 + +基于 **Go + Gin + GORM** 构建的 RESTful API 服务,负责商品管理、订单处理、用户认证、聊天消息等核心业务。 + +## 技术依赖 + +| 包 | 版本 | 用途 | +|----|------|------| +| gin | v1.9 | HTTP 路由框架 | +| gorm | v1.31 | ORM | +| gorm/driver/mysql | v1.6 | MySQL 驱动 | +| go-sql-driver/mysql | v1.9 | 底层 MySQL 连接 | +| gin-contrib/cors | latest | CORS 中间件 | +| google/uuid | latest | UUID 生成 | + +## 目录结构 + +``` +mengyastore-backend/ +├── main.go # 程序入口,路由注册 +├── cmd/ +│ └── migrate/ +│ └── main.go # 一次性 JSON→MySQL 数据迁移脚本 +├── data/ +│ └── json/ +│ └── settings.json # 服务配置(adminToken、DSN 等) +├── internal/ +│ ├── config/ +│ │ └── config.go # 配置加载 +│ ├── database/ +│ │ ├── db.go # GORM 初始化 + AutoMigrate +│ │ └── models.go # 数据库行结构体(GORM 模型) +│ ├── models/ +│ │ ├── product.go # 业务模型 Product +│ │ ├── order.go # 业务模型 Order +│ │ └── chat.go # 业务模型 ChatMessage +│ ├── storage/ +│ │ ├── jsonstore.go # 商品存储(GORM 实现) +│ │ ├── orderstore.go # 订单存储 +│ │ ├── sitestore.go # 站点设置存储 +│ │ ├── wishliststore.go # 收藏夹存储 +│ │ └── chatstore.go # 聊天消息存储(含内存级频率限制) +│ ├── handlers/ +│ │ ├── admin.go # AdminHandler 结构体 + requireAdmin +│ │ ├── admin_product.go # 商品 CRUD 接口 +│ │ ├── admin_site.go # 维护模式接口 +│ │ ├── admin_orders.go # 订单管理接口 +│ │ ├── admin_chat.go # 管理员聊天接口 +│ │ ├── public.go # 公开接口(商品列表、浏览量) +│ │ ├── order.go # 下单、确认订单接口 +│ │ ├── stats.go # 统计信息接口 +│ │ ├── wishlist.go # 收藏夹接口(用户) +│ │ └── chat.go # 聊天接口(用户) +│ └── auth/ +│ └── sproutgate.go # SproutGate OAuth 客户端 +``` + +## API 路由一览 + +### 公开接口 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/health` | 健康检查 | +| GET | `/api/products` | 获取商品列表(仅 active) | +| POST | `/api/products/:id/view` | 记录商品浏览量 | +| GET | `/api/stats` | 获取总订单数和总访问量 | +| POST | `/api/site/visit` | 记录站点访问 | +| GET | `/api/site/maintenance` | 获取维护状态 | +| POST | `/api/checkout` | 创建订单(生成支付二维码) | +| GET | `/api/orders` | 获取当前用户订单(需 Bearer token) | +| POST | `/api/orders/:id/confirm` | 确认付款(触发发货) | + +### 收藏夹(需登录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/wishlist` | 获取收藏商品 ID 列表 | +| POST | `/api/wishlist` | 添加收藏 | +| DELETE | `/api/wishlist/:id` | 取消收藏 | + +### 聊天(需登录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/chat/messages` | 获取自己的聊天记录 | +| POST | `/api/chat/messages` | 发送消息(1 秒频率限制) | + +### 管理员接口(需 `?token=xxx` 或 `Authorization: `) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/admin/token` | 获取令牌(用于验证) | +| GET | `/api/admin/products` | 获取全部商品(含卡密) | +| POST | `/api/admin/products` | 创建商品 | +| PUT | `/api/admin/products/:id` | 编辑商品 | +| PATCH | `/api/admin/products/:id/status` | 切换上下架 | +| DELETE | `/api/admin/products/:id` | 删除商品 | +| POST | `/api/admin/site/maintenance` | 设置维护模式 | +| GET | `/api/admin/orders` | 获取全部订单 | +| DELETE | `/api/admin/orders/:id` | 删除订单 | +| GET | `/api/admin/chat` | 获取全部用户对话 | +| GET | `/api/admin/chat/:account` | 获取指定用户对话 | +| POST | `/api/admin/chat/:account` | 管理员回复 | +| DELETE | `/api/admin/chat/:account` | 清除对话 | + +## 数据库表结构 + +### products + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | varchar(36) | UUID 主键 | +| name | varchar(255) | 商品名称 | +| price | double | 原价 | +| discount_price | double | 折扣价(0 = 无折扣)| +| tags | json | 标签数组 | +| cover_url | varchar(500) | 封面图 URL | +| screenshot_urls | json | 截图 URL 数组(最多 5 张)| +| verification_url | varchar(500) | 验证链接 | +| description | text | Markdown 描述 | +| active | tinyint(1) | 是否上架 | +| require_login | tinyint(1) | 是否必须登录购买 | +| max_per_account | bigint | 每账户最大购买数(0=不限)| +| total_sold | bigint | 累计销量 | +| view_count | bigint | 累计浏览量 | +| delivery_mode | varchar(20) | 发货模式:`auto` / `manual` | +| show_note | tinyint(1) | 下单时显示备注输入框 | +| show_contact | tinyint(1) | 下单时显示联系方式输入框 | +| created_at | datetime(3) | 创建时间 | + +### product_codes + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint unsigned | 自增主键 | +| product_id | varchar(36) | 关联商品 ID(索引)| +| code | text | 卡密内容 | + +### orders + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | varchar(36) | UUID 主键 | +| product_id | varchar(36) | 商品 ID(索引)| +| product_name | varchar(255) | 商品名称快照 | +| user_account | varchar(255) | 用户账号(可空,匿名)| +| user_name | varchar(255) | 用户昵称 | +| quantity | bigint | 购买数量 | +| delivered_codes | json | 已发放卡密 | +| status | varchar(20) | `pending` / `completed` | +| delivery_mode | varchar(20) | `auto` / `manual` | +| note | text | 用户备注 | +| contact_phone | varchar(50) | 联系手机号 | +| contact_email | varchar(255) | 联系邮箱 | +| created_at | datetime(3) | 下单时间 | + +### site_settings + +键值对存储,当前使用的键: + +| Key | 说明 | +|-----|------| +| `totalVisits` | 总访问量 | +| `maintenance` | 维护模式(`true` / `false`)| +| `maintenanceReason` | 维护原因文本 | + +### wishlists + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint unsigned | 自增主键 | +| account_id | varchar(255) | 用户账号(唯一索引)| +| product_id | varchar(36) | 商品 ID(联合唯一)| + +### chat_messages + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | varchar(36) | UUID 主键 | +| account_id | varchar(255) | 用户账号(索引)| +| account_name | varchar(255) | 用户昵称 | +| content | text | 消息内容 | +| sent_at | datetime(3) | 发送时间 | +| from_admin | tinyint(1) | 是否来自管理员 | + +## 配置文件 + +`data/json/settings.json`: + +```json +{ + "adminToken": "你的管理员令牌", + "authApiUrl": "https://auth.api.shumengya.top", + "databaseDsn": "" +} +``` + +`databaseDsn` 为空时自动使用测试数据库。也可以通过环境变量 `DATABASE_DSN` 覆盖。 + +## 发货逻辑 + +### 自动发货(`deliveryMode = "auto"`) + +1. `POST /api/checkout` → 从 `product_codes` 提取指定数量的卡密 +2. 商品 `quantity` 减少,卡密从数据库删除 +3. 卡密保存到订单 `delivered_codes` +4. 用户 `POST /api/orders/:id/confirm` 确认付款后,订单状态变为 `completed`,响应中返回卡密内容 +5. 同时调用 `IncrementSold` 增加销量统计 + +### 手动发货(`deliveryMode = "manual"`) + +1. `POST /api/checkout` → 创建订单,不提取卡密 +2. 用户 `POST /api/orders/:id/confirm` 后,订单变为 `completed`,但 `delivered_codes` 为空 +3. 管理员在后台查看订单的备注、手机号、邮箱后手动发货 + +## 本地开发 + +```bash +go run . # 启动服务(默认 :8080) +go build -o mengyastore-backend.exe . # 构建可执行文件 +go run ./cmd/migrate/main.go # 迁移旧 JSON 数据到数据库 +``` + +### 切换数据库 + +```bash +# 测试库(默认) +# host: 10.1.1.100:3306 / db: mengyastore-test + +# 生产库 +set DATABASE_DSN=mengyastore:mengyastore@tcp(192.168.1.100:3306)/mengyastore?charset=utf8mb4&parseTime=True&loc=Local +./mengyastore-backend.exe +``` + +## 认证说明 + +### 用户认证 + +通过 SproutGate OAuth 服务验证 Bearer Token: + +```go +result, err := authClient.VerifyToken(token) +// result.Valid, result.User.Account, result.User.Username +``` + +### 管理员认证 + +管理员令牌通过查询参数或 Authorization 头传入: + +``` +GET /api/admin/products?token=xxx +Authorization: xxx +``` + +令牌与 `settings.json` 中的 `adminToken` 比对。 diff --git a/mengyastore-backend/cmd/migrate/main.go b/mengyastore-backend/cmd/migrate/main.go new file mode 100644 index 0000000..5c203e6 --- /dev/null +++ b/mengyastore-backend/cmd/migrate/main.go @@ -0,0 +1,304 @@ +// migrate imports existing JSON data files into the MySQL database. +// Run once after switching to DB storage: +// +// go run ./cmd/migrate/main.go +package main + +import ( + "encoding/json" + "log" + "os" + "strconv" + "time" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/logger" + + "mengyastore-backend/internal/config" + "mengyastore-backend/internal/database" +) + +func main() { + cfg, err := config.Load("data/json/settings.json") + if err != nil { + log.Fatalf("load config: %v", err) + } + + db, err := gorm.Open(mysql.Open(cfg.DatabaseDSN), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + log.Fatalf("open db: %v", err) + } + + // Ensure tables exist + if err := db.AutoMigrate( + &database.ProductRow{}, + &database.ProductCodeRow{}, + &database.OrderRow{}, + &database.SiteSettingRow{}, + &database.WishlistRow{}, + &database.ChatMessageRow{}, + ); err != nil { + log.Fatalf("auto migrate: %v", err) + } + + log.Println("数据库连接成功,开始导入...") + migrateProducts(db) + migrateOrders(db) + migrateWishlists(db) + migrateChats(db) + migrateSite(db) + log.Println("✅ 数据导入完成!") +} + +// ─── Products ───────────────────────────────────────────────────────────────── + +type jsonProduct struct { + ID string `json:"id"` + Name string `json:"name"` + Price float64 `json:"price"` + DiscountPrice float64 `json:"discountPrice"` + Tags []string `json:"tags"` + CoverURL string `json:"coverUrl"` + ScreenshotURLs []string `json:"screenshotUrls"` + Description string `json:"description"` + Active bool `json:"active"` + RequireLogin bool `json:"requireLogin"` + MaxPerAccount int `json:"maxPerAccount"` + TotalSold int `json:"totalSold"` + ViewCount int `json:"viewCount"` + DeliveryMode string `json:"deliveryMode"` + ShowNote bool `json:"showNote"` + ShowContact bool `json:"showContact"` + Codes []string `json:"codes"` + CreatedAt time.Time `json:"createdAt"` +} + +func migrateProducts(db *gorm.DB) { + data, err := os.ReadFile("data/json/products.json") + if err != nil { + log.Printf("[products] 文件不存在,跳过: %v", err) + return + } + var products []jsonProduct + if err := json.Unmarshal(data, &products); err != nil { + log.Printf("[products] JSON 解析失败: %v", err) + return + } + for _, p := range products { + if p.ID == "" { + continue + } + if p.DeliveryMode == "" { + p.DeliveryMode = "auto" + } + row := database.ProductRow{ + ID: p.ID, + Name: p.Name, + Price: p.Price, + DiscountPrice: p.DiscountPrice, + Tags: database.StringSlice(p.Tags), + CoverURL: p.CoverURL, + ScreenshotURLs: database.StringSlice(p.ScreenshotURLs), + Description: p.Description, + Active: p.Active, + RequireLogin: p.RequireLogin, + MaxPerAccount: p.MaxPerAccount, + TotalSold: p.TotalSold, + ViewCount: p.ViewCount, + DeliveryMode: p.DeliveryMode, + ShowNote: p.ShowNote, + ShowContact: p.ShowContact, + CreatedAt: p.CreatedAt, + } + if row.CreatedAt.IsZero() { + row.CreatedAt = time.Now() + } + result := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&row) + if result.Error != nil { + log.Printf("[products] 导入 %s 失败: %v", p.ID, result.Error) + continue + } + // Codes → product_codes + for _, code := range p.Codes { + if code == "" { + continue + } + db.Clauses(clause.OnConflict{DoNothing: true}).Create(&database.ProductCodeRow{ + ProductID: p.ID, + Code: code, + }) + } + } + log.Printf("[products] 导入 %d 条商品", len(products)) +} + +// ─── Orders ─────────────────────────────────────────────────────────────────── + +type jsonOrder struct { + ID string `json:"id"` + ProductID string `json:"productId"` + ProductName string `json:"productName"` + UserAccount string `json:"userAccount"` + UserName string `json:"userName"` + Quantity int `json:"quantity"` + DeliveredCodes []string `json:"deliveredCodes"` + Status string `json:"status"` + DeliveryMode string `json:"deliveryMode"` + Note string `json:"note"` + ContactPhone string `json:"contactPhone"` + ContactEmail string `json:"contactEmail"` + CreatedAt time.Time `json:"createdAt"` +} + +func migrateOrders(db *gorm.DB) { + data, err := os.ReadFile("data/json/orders.json") + if err != nil { + log.Printf("[orders] 文件不存在,跳过: %v", err) + return + } + var orders []jsonOrder + if err := json.Unmarshal(data, &orders); err != nil { + log.Printf("[orders] JSON 解析失败: %v", err) + return + } + for _, o := range orders { + if o.ID == "" { + continue + } + if o.DeliveryMode == "" { + o.DeliveryMode = "auto" + } + if o.DeliveredCodes == nil { + o.DeliveredCodes = []string{} + } + row := database.OrderRow{ + ID: o.ID, + ProductID: o.ProductID, + ProductName: o.ProductName, + UserAccount: o.UserAccount, + UserName: o.UserName, + Quantity: o.Quantity, + DeliveredCodes: database.StringSlice(o.DeliveredCodes), + Status: o.Status, + DeliveryMode: o.DeliveryMode, + Note: o.Note, + ContactPhone: o.ContactPhone, + ContactEmail: o.ContactEmail, + CreatedAt: o.CreatedAt, + } + if row.CreatedAt.IsZero() { + row.CreatedAt = time.Now() + } + if result := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&row); result.Error != nil { + log.Printf("[orders] 导入 %s 失败: %v", o.ID, result.Error) + } + } + log.Printf("[orders] 导入 %d 条订单", len(orders)) +} + +// ─── Wishlists ──────────────────────────────────────────────────────────────── + +func migrateWishlists(db *gorm.DB) { + data, err := os.ReadFile("data/json/wishlists.json") + if err != nil { + log.Printf("[wishlists] 文件不存在,跳过: %v", err) + return + } + var wl map[string][]string + if err := json.Unmarshal(data, &wl); err != nil { + log.Printf("[wishlists] JSON 解析失败: %v", err) + return + } + count := 0 + for account, productIDs := range wl { + for _, pid := range productIDs { + db.Clauses(clause.OnConflict{DoNothing: true}).Create(&database.WishlistRow{ + AccountID: account, + ProductID: pid, + }) + count++ + } + } + log.Printf("[wishlists] 导入 %d 条收藏记录", count) +} + +// ─── Chats ──────────────────────────────────────────────────────────────────── + +type jsonChatMsg struct { + ID string `json:"id"` + AccountID string `json:"accountId"` + AccountName string `json:"accountName"` + Content string `json:"content"` + SentAt time.Time `json:"sentAt"` + FromAdmin bool `json:"fromAdmin"` +} + +func migrateChats(db *gorm.DB) { + data, err := os.ReadFile("data/json/chats.json") + if err != nil { + log.Printf("[chats] 文件不存在,跳过: %v", err) + return + } + var convs map[string][]jsonChatMsg + if err := json.Unmarshal(data, &convs); err != nil { + log.Printf("[chats] JSON 解析失败: %v", err) + return + } + count := 0 + for _, msgs := range convs { + for _, m := range msgs { + if m.ID == "" { + continue + } + db.Clauses(clause.OnConflict{DoNothing: true}).Create(&database.ChatMessageRow{ + ID: m.ID, + AccountID: m.AccountID, + AccountName: m.AccountName, + Content: m.Content, + SentAt: m.SentAt, + FromAdmin: m.FromAdmin, + }) + count++ + } + } + log.Printf("[chats] 导入 %d 条聊天消息", count) +} + +// ─── Site settings ──────────────────────────────────────────────────────────── + +type jsonSite struct { + TotalVisits int `json:"totalVisits"` + Maintenance bool `json:"maintenance"` + MaintenanceReason string `json:"maintenanceReason"` +} + +func migrateSite(db *gorm.DB) { + data, err := os.ReadFile("data/json/site.json") + if err != nil { + log.Printf("[site] 文件不存在,跳过: %v", err) + return + } + var site jsonSite + if err := json.Unmarshal(data, &site); err != nil { + log.Printf("[site] JSON 解析失败: %v", err) + return + } + upsert := func(key, value string) { + db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "key"}}, + DoUpdates: clause.AssignmentColumns([]string{"value"}), + }).Create(&database.SiteSettingRow{Key: key, Value: value}) + } + upsert("totalVisits", strconv.Itoa(site.TotalVisits)) + maintenance := "false" + if site.Maintenance { + maintenance = "true" + } + upsert("maintenance", maintenance) + upsert("maintenanceReason", site.MaintenanceReason) + log.Printf("[site] 站点设置导入完成(访问量: %d)", site.TotalVisits) +} diff --git a/mengyastore-backend/data/json/orders.json b/mengyastore-backend/data/json/orders.json deleted file mode 100644 index 8abe671..0000000 --- a/mengyastore-backend/data/json/orders.json +++ /dev/null @@ -1,119 +0,0 @@ -[ - { - "id": "0bea9606-51aa-4fe2-a932-ab0e36ee33ca", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "", - "userName": "", - "quantity": 1, - "deliveredCodes": [ - "LINUX-INVITE-001" - ], - "status": "pending", - "createdAt": "2026-03-19T17:23:46.1743551+08:00" - }, - { - "id": "5be3ecbd-873b-4ea2-9209-e96f6eb528cd", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "", - "userName": "", - "quantity": 1, - "deliveredCodes": [ - "LINUX-INVITE-002" - ], - "status": "pending", - "createdAt": "2026-03-19T17:24:07.6045189+08:00" - }, - { - "id": "c0cbb6c7-76be-49ef-9e67-8d2ae890e555", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "", - "userName": "", - "quantity": 1, - "deliveredCodes": [ - "啊伟大伟大伟大我" - ], - "status": "pending", - "createdAt": "2026-03-19T22:28:28.5393405+08:00" - }, - { - "id": "f299bbb4-0de4-4824-84ab-d1ccfb3b35dd", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "", - "userName": "", - "quantity": 1, - "deliveredCodes": [ - "啊伟大伟大伟大伟大" - ], - "status": "pending", - "createdAt": "2026-03-20T10:32:38.352837+08:00" - }, - { - "id": "413931af-2867-4855-89af-515747d4b5e5", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "", - "userName": "", - "quantity": 1, - "deliveredCodes": [ - "你是傻逼哈哈哈被骗了吧" - ], - "status": "pending", - "createdAt": "2026-03-20T10:32:55.2785291+08:00" - }, - { - "id": "59ab54e0-8b98-48d3-bf63-a843ef2c95a4", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "", - "userName": "", - "quantity": 1, - "deliveredCodes": [ - "唐" - ], - "status": "pending", - "createdAt": "2026-03-20T10:39:37.9977301+08:00" - }, - { - "id": "94e82c71-8237-429f-b593-2530314b72af", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "", - "userName": "", - "quantity": 1, - "deliveredCodes": [ - "原神牛逼" - ], - "status": "completed", - "createdAt": "2026-03-20T10:40:45.3820749+08:00" - }, - { - "id": "058cad17-608c-4108-b012-af42f688a047", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "shumengya", - "userName": "树萌芽", - "quantity": 1, - "deliveredCodes": [ - "123123123131" - ], - "status": "completed", - "createdAt": "2026-03-20T10:44:21.375082+08:00" - }, - { - "id": "e95f30ab-da4f-4dec-872c-3c9047cd8193", - "productId": "seed-1", - "productName": "Linux Do 邀请码", - "userAccount": "shumengya", - "userName": "树萌芽", - "quantity": 1, - "deliveredCodes": [ - "131231231231231" - ], - "status": "completed", - "createdAt": "2026-03-20T10:57:13.3436565+08:00" - } -] \ No newline at end of file diff --git a/mengyastore-backend/data/json/products.json b/mengyastore-backend/data/json/products.json deleted file mode 100644 index d3a9363..0000000 --- a/mengyastore-backend/data/json/products.json +++ /dev/null @@ -1,181 +0,0 @@ -[ - { - "id": "seed-1", - "name": "Linux Do 邀请码", - "price": 7, - "discountPrice": 4, - "tags": [ - "邀请码", - "LinuxDo" - ], - "quantity": 0, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [], - "viewCount": 10, - "description": "Linux.do论坛邀请码 默认每天可以生成一个,先到先得.", - "active": true, - "createdAt": "2026-03-15T10:00:00+08:00", - "updatedAt": "2026-03-20T11:37:16.2219815+08:00" - }, - { - "id": "seed-2", - "name": "ChatGPT普号", - "price": 1, - "discountPrice": 0, - "tags": [], - "quantity": 0, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [], - "viewCount": 2, - "description": "ChatGPT 普号 纯手工注册 数量不多", - "active": true, - "createdAt": "2026-03-15T10:05:00+08:00", - "updatedAt": "2026-03-20T11:34:54.3522714+08:00" - }, - { - "id": "2b6b6051-bca7-42da-b127-c7b721c50c06", - "name": "谷歌账号", - "price": 20, - "discountPrice": 0, - "tags": [], - "quantity": 0, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [], - "viewCount": 1, - "description": "谷歌账号 现货 可绑定F2A验证", - "active": true, - "createdAt": "2026-03-15T20:52:52.0381722+08:00", - "updatedAt": "2026-03-19T19:33:05.6844325+08:00" - }, - { - "id": "b9922892-c197-44be-be87-637ccb6bebeb", - "name": "萌芽币", - "price": 999999, - "discountPrice": 0, - "tags": [], - "quantity": 0, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [], - "viewCount": 1, - "description": "非买品 仅展示", - "active": true, - "createdAt": "2026-03-15T21:03:00.0164528+08:00", - "updatedAt": "2026-03-19T19:33:07.508758+08:00" - }, - { - "id": "ee8e0140-221c-4bfa-b10a-13b1f98ea4e5", - "name": "Keep校园跑 代刷4公里", - "price": 1, - "discountPrice": 0, - "tags": [], - "quantity": 0, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [], - "viewCount": 1, - "description": "keep校园跑带刷 每天4-5公里 下单后直接联系我发账号", - "active": true, - "createdAt": "2026-03-15T21:06:11.9820102+08:00", - "updatedAt": "2026-03-19T19:33:09.1800225+08:00" - }, - { - "id": "00bbf5db-b99e-4e88-a8ee-e7747b5969fe", - "name": "学习通/慕课挂课脚本", - "price": 25, - "discountPrice": 0, - "tags": [], - "quantity": 0, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [], - "viewCount": 1, - "description": "学习通,慕课挂科脚本 手机 电脑都可以挂 不会弄可联系教你", - "active": true, - "createdAt": "2026-03-15T21:06:45.3807471+08:00", - "updatedAt": "2026-03-19T19:33:02.9673884+08:00" - }, - { - "id": "6c7bf494-ef2c-4221-9bf7-ec3c94070d25", - "name": "smyhub.com后缀域名邮箱", - "price": 5, - "discountPrice": 0, - "tags": [], - "quantity": 0, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [], - "viewCount": 1, - "description": "纪念意义,比如我自己的mail@smyhub.com 目前已经续费了5年到2031年", - "active": true, - "createdAt": "2026-03-18T22:17:41.3034538+08:00", - "updatedAt": "2026-03-19T19:32:26.7674929+08:00" - }, - { - "id": "a30a2275-1c9c-49e4-a402-3e446e3e0f5c", - "name": "萌芽账号邀请码", - "price": 10, - "discountPrice": 8, - "tags": [], - "quantity": 1, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [ - "原神牛逼" - ], - "viewCount": 0, - "description": "萌芽统一账号登录平台邀请码", - "active": true, - "createdAt": "2026-03-20T11:04:05.5787516+08:00", - "updatedAt": "2026-03-20T11:04:05.5787516+08:00" - }, - { - "id": "bcd5d73b-6ad9-4ed9-8e18-42ea0482ceb3", - "name": "Keep 代跑脚本", - "price": 50, - "discountPrice": 0, - "tags": [], - "quantity": 1, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [ - "傻逼" - ], - "viewCount": 0, - "description": "Keep 校园跑脚本", - "active": true, - "createdAt": "2026-03-20T11:17:36.1915376+08:00", - "updatedAt": "2026-03-20T11:17:36.1915376+08:00" - }, - { - "id": "7ab90d55-92c1-49d3-9d0a-01e5b1c08340", - "name": "原神牛逼", - "price": 0, - "discountPrice": 0, - "tags": [], - "quantity": 1, - "coverUrl": "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png", - "screenshotUrls": [], - "verificationUrl": "", - "codes": [ - "原神牛逼" - ], - "viewCount": 0, - "description": "购买后直接发送一句原神牛逼", - "active": true, - "createdAt": "2026-03-20T11:36:36.6726035+08:00", - "updatedAt": "2026-03-20T11:42:05.3303102+08:00" - } -] \ No newline at end of file diff --git a/mengyastore-backend/data/json/site.json b/mengyastore-backend/data/json/site.json deleted file mode 100644 index 05c3c4a..0000000 --- a/mengyastore-backend/data/json/site.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "totalVisits": 3 -} \ No newline at end of file diff --git a/mengyastore-backend/docker-compose.yml b/mengyastore-backend/docker-compose.yml index 6df578c..64c3ea8 100644 --- a/mengyastore-backend/docker-compose.yml +++ b/mengyastore-backend/docker-compose.yml @@ -8,6 +8,9 @@ services: environment: GIN_MODE: release TZ: Asia/Shanghai + # Production MySQL DSN — uses internal network address. + # Change to TestDSN or override via .env file for local testing. + DATABASE_DSN: "mengyastore:mengyastore@tcp(192.168.1.100:3306)/mengyastore?charset=utf8mb4&parseTime=True&loc=Local" volumes: - - ./data:/app/data + - ./config.json:/app/config.json:ro restart: unless-stopped diff --git a/mengyastore-backend/go.mod b/mengyastore-backend/go.mod index 331c63c..ee56f87 100644 --- a/mengyastore-backend/go.mod +++ b/mengyastore-backend/go.mod @@ -1,6 +1,6 @@ module mengyastore-backend -go 1.21 +go 1.21.0 require ( github.com/gin-contrib/cors v1.7.2 @@ -9,6 +9,7 @@ require ( ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect @@ -18,7 +19,10 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kr/text v0.2.0 // indirect @@ -33,7 +37,9 @@ require ( golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.20.0 // indirect google.golang.org/protobuf v1.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect ) diff --git a/mengyastore-backend/go.sum b/mengyastore-backend/go.sum index 83f7fd1..c1afd06 100644 --- a/mengyastore-backend/go.sum +++ b/mengyastore-backend/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -26,6 +28,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= @@ -33,6 +37,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -87,6 +95,8 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= @@ -97,5 +107,9 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/mengyastore-backend/internal/auth/sproutgate.go b/mengyastore-backend/internal/auth/sproutgate.go index 124e671..6b672aa 100644 --- a/mengyastore-backend/internal/auth/sproutgate.go +++ b/mengyastore-backend/internal/auth/sproutgate.go @@ -26,6 +26,8 @@ type SproutGateUser struct { Account string `json:"account"` Username string `json:"username"` AvatarURL string `json:"avatarUrl"` + Level int `json:"level"` + Email string `json:"email"` } func NewSproutGateClient(apiURL string) *SproutGateClient { diff --git a/mengyastore-backend/internal/config/config.go b/mengyastore-backend/internal/config/config.go index fdc8266..c672df5 100644 --- a/mengyastore-backend/internal/config/config.go +++ b/mengyastore-backend/internal/config/config.go @@ -9,8 +9,18 @@ import ( type Config struct { AdminToken string `json:"adminToken"` AuthAPIURL string `json:"authApiUrl"` + + // Database DSN. If empty, falls back to the test DB DSN. + // Format: "user:pass@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local" + DatabaseDSN string `json:"databaseDsn"` } +// Default DSNs for each environment. +const ( + TestDSN = "mengyastore-test:mengyastore-test@tcp(10.1.1.100:3306)/mengyastore-test?charset=utf8mb4&parseTime=True&loc=Local" + ProdDSN = "mengyastore:mengyastore@tcp(192.168.1.100:3306)/mengyastore?charset=utf8mb4&parseTime=True&loc=Local" +) + func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { @@ -23,5 +33,12 @@ func Load(path string) (*Config, error) { if cfg.AdminToken == "" { cfg.AdminToken = "shumengya520" } + // Default to test DB if not configured; environment variable overrides config file. + if dsn := os.Getenv("DATABASE_DSN"); dsn != "" { + cfg.DatabaseDSN = dsn + } + if cfg.DatabaseDSN == "" { + cfg.DatabaseDSN = TestDSN + } return &cfg, nil } diff --git a/mengyastore-backend/internal/database/db.go b/mengyastore-backend/internal/database/db.go new file mode 100644 index 0000000..0ea56f9 --- /dev/null +++ b/mengyastore-backend/internal/database/db.go @@ -0,0 +1,45 @@ +package database + +import ( + "log" + "time" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// Open initialises a GORM DB connection and runs AutoMigrate for all models. +func Open(dsn string) (*gorm.DB, error) { + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Warn), + }) + if err != nil { + return nil, err + } + + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + sqlDB.SetMaxIdleConns(5) + sqlDB.SetMaxOpenConns(20) + sqlDB.SetConnMaxLifetime(time.Hour) + + if err := autoMigrate(db); err != nil { + return nil, err + } + log.Println("[DB] 数据库连接成功,表结构已同步") + return db, nil +} + +func autoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &ProductRow{}, + &ProductCodeRow{}, + &OrderRow{}, + &SiteSettingRow{}, + &WishlistRow{}, + &ChatMessageRow{}, + ) +} diff --git a/mengyastore-backend/internal/database/models.go b/mengyastore-backend/internal/database/models.go new file mode 100644 index 0000000..3f58378 --- /dev/null +++ b/mengyastore-backend/internal/database/models.go @@ -0,0 +1,121 @@ +package database + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" +) + +// StringSlice is a JSON-serialized string slice stored as a MySQL TEXT/JSON column. +type StringSlice []string + +func (s StringSlice) Value() (driver.Value, error) { + if s == nil { + return "[]", nil + } + b, err := json.Marshal(s) + return string(b), err +} + +func (s *StringSlice) Scan(src any) error { + var raw []byte + switch v := src.(type) { + case string: + raw = []byte(v) + case []byte: + raw = v + default: + return fmt.Errorf("StringSlice: unsupported type %T", src) + } + return json.Unmarshal(raw, s) +} + +// ─── Products ──────────────────────────────────────────────────────────────── + +// ProductRow is the GORM model for the `products` table. +type ProductRow struct { + ID string `gorm:"primaryKey;size:36"` + Name string `gorm:"size:255;not null"` + Price float64 `gorm:"not null;default:0"` + DiscountPrice float64 `gorm:"default:0"` + Tags StringSlice `gorm:"type:json"` + CoverURL string `gorm:"size:500"` + ScreenshotURLs StringSlice `gorm:"type:json"` + VerificationURL string `gorm:"size:500;default:''"` + Description string `gorm:"type:text"` + Active bool `gorm:"default:true;index"` + RequireLogin bool `gorm:"default:false"` + MaxPerAccount int `gorm:"default:0"` + TotalSold int `gorm:"default:0"` + ViewCount int `gorm:"default:0"` + DeliveryMode string `gorm:"size:20;default:'auto'"` + ShowNote bool `gorm:"default:false"` + ShowContact bool `gorm:"default:false"` + CreatedAt time.Time `gorm:"index"` +} + +func (ProductRow) TableName() string { return "products" } + +// ProductCodeRow stores individual codes for a product (one row per code). +type ProductCodeRow struct { + ID uint `gorm:"primaryKey;autoIncrement"` + ProductID string `gorm:"size:36;not null;index"` + Code string `gorm:"type:text;not null"` +} + +func (ProductCodeRow) TableName() string { return "product_codes" } + +// ─── Orders ────────────────────────────────────────────────────────────────── + +type OrderRow struct { + ID string `gorm:"primaryKey;size:36"` + ProductID string `gorm:"size:36;not null;index"` + ProductName string `gorm:"size:255;not null"` + UserAccount string `gorm:"size:255;index"` + UserName string `gorm:"size:255"` + Quantity int `gorm:"not null;default:1"` + DeliveredCodes StringSlice `gorm:"type:json"` + Status string `gorm:"size:20;not null;default:'pending';index"` + DeliveryMode string `gorm:"size:20;default:'auto'"` + Note string `gorm:"type:text"` + ContactPhone string `gorm:"size:50"` + ContactEmail string `gorm:"size:255"` + NotifyEmail string `gorm:"size:255"` + CreatedAt time.Time +} + +func (OrderRow) TableName() string { return "orders" } + +// ─── Site settings ─────────────────────────────────────────────────────────── + +// SiteSettingRow stores arbitrary key-value pairs for site-wide settings. +type SiteSettingRow struct { + Key string `gorm:"primaryKey;size:64"` + Value string `gorm:"type:text"` +} + +func (SiteSettingRow) TableName() string { return "site_settings" } + +// ─── Wishlists ─────────────────────────────────────────────────────────────── + +type WishlistRow struct { + ID uint `gorm:"primaryKey;autoIncrement"` + AccountID string `gorm:"size:255;not null;index:idx_wishlist,unique"` + ProductID string `gorm:"size:36;not null;index:idx_wishlist,unique"` +} + +func (WishlistRow) TableName() string { return "wishlists" } + +// ─── Chat messages ─────────────────────────────────────────────────────────── + +type ChatMessageRow struct { + ID string `gorm:"primaryKey;size:36"` + AccountID string `gorm:"size:255;not null;index"` + AccountName string `gorm:"size:255"` + Content string `gorm:"type:text;not null"` + SentAt time.Time `gorm:"not null"` + FromAdmin bool `gorm:"default:false"` +} + +func (ChatMessageRow) TableName() string { return "chat_messages" } diff --git a/mengyastore-backend/internal/email/email.go b/mengyastore-backend/internal/email/email.go new file mode 100644 index 0000000..b47eb9c --- /dev/null +++ b/mengyastore-backend/internal/email/email.go @@ -0,0 +1,192 @@ +package email + +import ( + "crypto/tls" + "fmt" + "net/smtp" + "strings" + "time" +) + +// Config holds SMTP sender configuration. +type Config struct { + SMTPHost string // e.g. smtp.qq.com + SMTPPort string // e.g. 465 (SSL) or 587 (STARTTLS) + From string // sender email address + Password string // SMTP auth password / app password + FromName string // display name, e.g. "萌芽小店" +} + +// IsConfigured returns true if enough config is present to send mail. +func (c *Config) IsConfigured() bool { + return c.From != "" && c.Password != "" && c.SMTPHost != "" +} + +// OrderNotifyData contains the data for an order notification email. +type OrderNotifyData struct { + ToEmail string + ToName string + ProductName string + OrderID string + Quantity int + Codes []string // empty for manual delivery + IsManual bool +} + +// SendOrderNotify sends an order delivery notification email. +// Returns nil if config is not ready or ToEmail is empty (silently skip). +func SendOrderNotify(cfg Config, data OrderNotifyData) error { + if !cfg.IsConfigured() || data.ToEmail == "" { + return nil + } + + if cfg.SMTPPort == "" { + cfg.SMTPPort = "465" + } + if cfg.SMTPHost == "" { + cfg.SMTPHost = "smtp.qq.com" + } + fromName := cfg.FromName + if fromName == "" { + fromName = "萌芽小店" + } + + subject := "【萌芽小店】您的订单已发货" + if data.IsManual { + subject = "【萌芽小店】您的订单正在处理中" + } + + body := buildBody(data) + + msg := buildMIMEMessage(cfg.From, fromName, data.ToEmail, subject, body) + + addr := fmt.Sprintf("%s:%s", cfg.SMTPHost, cfg.SMTPPort) + auth := smtp.PlainAuth("", cfg.From, cfg.Password, cfg.SMTPHost) + + // QQ mail uses SSL on port 465; use TLS dial directly. + if cfg.SMTPPort == "465" { + return sendSSL(addr, cfg.SMTPHost, auth, cfg.From, data.ToEmail, msg) + } + return smtp.SendMail(addr, auth, cfg.From, []string{data.ToEmail}, []byte(msg)) +} + +func buildBody(data OrderNotifyData) string { + var sb strings.Builder + now := time.Now().Format("2006 年 01 月 02 日 15:04:05") + + recipient := data.ToName + if recipient == "" { + recipient = "用户" + } + + sb.WriteString("尊敬的 ") + sb.WriteString(recipient) + sb.WriteString(",\n\n") + sb.WriteString(" 您好!感谢您在萌芽小店的支持与购买。\n\n") + + sb.WriteString("────────────────────────────────\n") + sb.WriteString(" 订单信息\n") + sb.WriteString("────────────────────────────────\n") + sb.WriteString(fmt.Sprintf(" 商品名称:%s\n", data.ProductName)) + sb.WriteString(fmt.Sprintf(" 订单编号:%s\n", data.OrderID)) + sb.WriteString(fmt.Sprintf(" 购买数量:%d 件\n", data.Quantity)) + sb.WriteString(fmt.Sprintf(" 通知时间:%s\n", now)) + sb.WriteString("────────────────────────────────\n\n") + + if data.IsManual { + sb.WriteString(" 您的订单已成功提交,目前正在等待人工审核与处理。\n") + sb.WriteString(" 工作人员将尽快为您安排发货,请耐心等候。\n") + sb.WriteString(" 发货完成后,我们将另行发送邮件通知。\n\n") + } else { + sb.WriteString(" 您的订单已完成自动发货,发货内容如下:\n\n") + if len(data.Codes) > 0 { + for i, code := range data.Codes { + sb.WriteString(fmt.Sprintf(" [%d] %s\n", i+1, code)) + } + sb.WriteString("\n") + } + sb.WriteString(" 请妥善保管以上发货内容,切勿泄露给他人。\n\n") + } + + sb.WriteString(" 如有任何疑问,请联系在线客服,我们将竭诚为您服务。\n\n") + sb.WriteString("────────────────────────────────\n") + sb.WriteString(" 此邮件由系统自动发送,请勿直接回复。\n") + sb.WriteString("────────────────────────────────\n") + return sb.String() +} + +func buildMIMEMessage(from, fromName, to, subject, body string) string { + encodedFromName := fmt.Sprintf("=?UTF-8?B?%s?=", encodeBase64(fromName)) + encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", encodeBase64(subject)) + return fmt.Sprintf( + "From: %s <%s>\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: base64\r\n\r\n%s", + encodedFromName, from, to, encodedSubject, encodeBase64(body), + ) +} + +func encodeBase64(s string) string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + b := []byte(s) + var buf strings.Builder + for i := 0; i < len(b); i += 3 { + remaining := len(b) - i + b0 := b[i] + b1 := byte(0) + b2 := byte(0) + if remaining > 1 { + b1 = b[i+1] + } + if remaining > 2 { + b2 = b[i+2] + } + buf.WriteByte(chars[b0>>2]) + buf.WriteByte(chars[((b0&0x03)<<4)|(b1>>4)]) + if remaining > 1 { + buf.WriteByte(chars[((b1&0x0f)<<2)|(b2>>6)]) + } else { + buf.WriteByte('=') + } + if remaining > 2 { + buf.WriteByte(chars[b2&0x3f]) + } else { + buf.WriteByte('=') + } + } + return buf.String() +} + +func sendSSL(addr, host string, auth smtp.Auth, from, to string, msg string) error { + tlsConfig := &tls.Config{ + ServerName: host, + MinVersion: tls.VersionTLS12, + } + conn, err := tls.Dial("tcp", addr, tlsConfig) + if err != nil { + return fmt.Errorf("tls dial: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, host) + if err != nil { + return fmt.Errorf("smtp new client: %w", err) + } + defer client.Quit() //nolint:errcheck + + if err = client.Auth(auth); err != nil { + return fmt.Errorf("smtp auth: %w", err) + } + if err = client.Mail(from); err != nil { + return fmt.Errorf("smtp MAIL FROM: %w", err) + } + if err = client.Rcpt(to); err != nil { + return fmt.Errorf("smtp RCPT TO: %w", err) + } + w, err := client.Data() + if err != nil { + return fmt.Errorf("smtp DATA: %w", err) + } + if _, err = fmt.Fprint(w, msg); err != nil { + return fmt.Errorf("smtp write body: %w", err) + } + return w.Close() +} diff --git a/mengyastore-backend/internal/handlers/admin.go b/mengyastore-backend/internal/handlers/admin.go index c704cf1..a159cee 100644 --- a/mengyastore-backend/internal/handlers/admin.go +++ b/mengyastore-backend/internal/handlers/admin.go @@ -1,160 +1,24 @@ package handlers import ( - "net/http" - "strings" - "github.com/gin-gonic/gin" + "net/http" "mengyastore-backend/internal/config" - "mengyastore-backend/internal/models" "mengyastore-backend/internal/storage" ) +// AdminHandler holds dependencies for all admin-related routes. type AdminHandler struct { - store *storage.JSONStore - cfg *config.Config + store *storage.JSONStore + cfg *config.Config + siteStore *storage.SiteStore + orderStore *storage.OrderStore + chatStore *storage.ChatStore } -type productPayload struct { - Name string `json:"name"` - Price float64 `json:"price"` - DiscountPrice float64 `json:"discountPrice"` - Tags string `json:"tags"` - CoverURL string `json:"coverUrl"` - Codes []string `json:"codes"` - ScreenshotURLs []string `json:"screenshotUrls"` - Description string `json:"description"` - Active *bool `json:"active"` -} - -type togglePayload struct { - Active bool `json:"active"` -} - -func NewAdminHandler(store *storage.JSONStore, cfg *config.Config) *AdminHandler { - return &AdminHandler{store: store, cfg: cfg} -} - -func (h *AdminHandler) GetAdminToken(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"token": h.cfg.AdminToken}) -} - -func (h *AdminHandler) ListAllProducts(c *gin.Context) { - if !h.requireAdmin(c) { - return - } - items, err := h.store.ListAll() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"data": items}) -} - -func (h *AdminHandler) CreateProduct(c *gin.Context) { - if !h.requireAdmin(c) { - return - } - var payload productPayload - if err := c.ShouldBindJSON(&payload); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) - return - } - screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) - if !valid { - c.JSON(http.StatusBadRequest, gin.H{"error": "screenshot urls must be 5 or fewer"}) - return - } - active := true - if payload.Active != nil { - active = *payload.Active - } - product := models.Product{ - Name: payload.Name, - Price: payload.Price, - DiscountPrice: payload.DiscountPrice, - Tags: normalizeTags(payload.Tags), - CoverURL: strings.TrimSpace(payload.CoverURL), - Codes: payload.Codes, - ScreenshotURLs: screenshotURLs, - Description: payload.Description, - Active: active, - } - created, err := h.store.Create(product) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"data": created}) -} - -func (h *AdminHandler) UpdateProduct(c *gin.Context) { - if !h.requireAdmin(c) { - return - } - id := c.Param("id") - var payload productPayload - if err := c.ShouldBindJSON(&payload); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) - return - } - screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) - if !valid { - c.JSON(http.StatusBadRequest, gin.H{"error": "screenshot urls must be 5 or fewer"}) - return - } - active := false - if payload.Active != nil { - active = *payload.Active - } - patch := models.Product{ - Name: payload.Name, - Price: payload.Price, - DiscountPrice: payload.DiscountPrice, - Tags: normalizeTags(payload.Tags), - CoverURL: strings.TrimSpace(payload.CoverURL), - Codes: payload.Codes, - ScreenshotURLs: screenshotURLs, - Description: payload.Description, - Active: active, - } - updated, err := h.store.Update(id, patch) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"data": updated}) -} - -func (h *AdminHandler) ToggleProduct(c *gin.Context) { - if !h.requireAdmin(c) { - return - } - id := c.Param("id") - var payload togglePayload - if err := c.ShouldBindJSON(&payload); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) - return - } - updated, err := h.store.Toggle(id, payload.Active) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"data": updated}) -} - -func (h *AdminHandler) DeleteProduct(c *gin.Context) { - if !h.requireAdmin(c) { - return - } - id := c.Param("id") - if err := h.store.Delete(id); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"status": "deleted"}) +func NewAdminHandler(store *storage.JSONStore, cfg *config.Config, siteStore *storage.SiteStore, orderStore *storage.OrderStore, chatStore *storage.ChatStore) *AdminHandler { + return &AdminHandler{store: store, cfg: cfg, siteStore: siteStore, orderStore: orderStore, chatStore: chatStore} } func (h *AdminHandler) requireAdmin(c *gin.Context) bool { @@ -168,43 +32,3 @@ func (h *AdminHandler) requireAdmin(c *gin.Context) bool { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return false } - -func normalizeScreenshotURLs(urls []string) ([]string, bool) { - cleaned := make([]string, 0, len(urls)) - for _, url := range urls { - trimmed := strings.TrimSpace(url) - if trimmed == "" { - continue - } - cleaned = append(cleaned, trimmed) - if len(cleaned) > 5 { - return nil, false - } - } - return cleaned, true -} - -func normalizeTags(tagsCSV string) []string { - if tagsCSV == "" { - return []string{} - } - parts := strings.Split(tagsCSV, ",") - clean := make([]string, 0, len(parts)) - seen := map[string]bool{} - for _, p := range parts { - t := strings.TrimSpace(p) - if t == "" { - continue - } - key := strings.ToLower(t) - if seen[key] { - continue - } - seen[key] = true - clean = append(clean, t) - if len(clean) >= 20 { - break - } - } - return clean -} diff --git a/mengyastore-backend/internal/handlers/admin_chat.go b/mengyastore-backend/internal/handlers/admin_chat.go new file mode 100644 index 0000000..098841a --- /dev/null +++ b/mengyastore-backend/internal/handlers/admin_chat.go @@ -0,0 +1,88 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// GetAllConversations returns all conversations (map of accountID -> messages). +func (h *AdminHandler) GetAllConversations(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + convs, err := h.chatStore.ListConversations() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"conversations": convs}}) +} + +// GetConversation returns all messages for a specific account. +func (h *AdminHandler) GetConversation(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + accountID := c.Param("account") + if accountID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing account"}) + return + } + msgs, err := h.chatStore.GetMessages(accountID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"messages": msgs}}) +} + +type adminChatPayload struct { + Content string `json:"content"` +} + +// AdminReply sends a reply from admin to a specific user. +func (h *AdminHandler) AdminReply(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + accountID := c.Param("account") + if accountID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing account"}) + return + } + var payload adminChatPayload + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + content := strings.TrimSpace(payload.Content) + if content == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "消息不能为空"}) + return + } + msg, err := h.chatStore.SendAdminMessage(accountID, content) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"message": msg}}) +} + +// ClearConversation deletes all messages with a specific user. +func (h *AdminHandler) ClearConversation(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + accountID := c.Param("account") + if accountID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing account"}) + return + } + if err := h.chatStore.ClearConversation(accountID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} diff --git a/mengyastore-backend/internal/handlers/admin_orders.go b/mengyastore-backend/internal/handlers/admin_orders.go new file mode 100644 index 0000000..da70fa9 --- /dev/null +++ b/mengyastore-backend/internal/handlers/admin_orders.go @@ -0,0 +1,35 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func (h *AdminHandler) ListAllOrders(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + orders, err := h.orderStore.ListAll() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": orders}) +} + +func (h *AdminHandler) DeleteOrder(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + orderID := c.Param("id") + if orderID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing order id"}) + return + } + if err := h.orderStore.Delete(orderID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) +} diff --git a/mengyastore-backend/internal/handlers/admin_product.go b/mengyastore-backend/internal/handlers/admin_product.go new file mode 100644 index 0000000..6e8e330 --- /dev/null +++ b/mengyastore-backend/internal/handlers/admin_product.go @@ -0,0 +1,202 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "mengyastore-backend/internal/models" +) + +type productPayload struct { + Name string `json:"name"` + Price float64 `json:"price"` + DiscountPrice float64 `json:"discountPrice"` + Tags string `json:"tags"` + CoverURL string `json:"coverUrl"` + Codes []string `json:"codes"` + ScreenshotURLs []string `json:"screenshotUrls"` + Description string `json:"description"` + Active *bool `json:"active"` + RequireLogin bool `json:"requireLogin"` + MaxPerAccount int `json:"maxPerAccount"` + DeliveryMode string `json:"deliveryMode"` + ShowNote bool `json:"showNote"` + ShowContact bool `json:"showContact"` +} + +type togglePayload struct { + Active bool `json:"active"` +} + +func (h *AdminHandler) GetAdminToken(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"token": h.cfg.AdminToken}) +} + +func (h *AdminHandler) ListAllProducts(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + items, err := h.store.ListAll() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": items}) +} + +func (h *AdminHandler) CreateProduct(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + var payload productPayload + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) + if !valid { + c.JSON(http.StatusBadRequest, gin.H{"error": "screenshot urls must be 5 or fewer"}) + return + } + active := true + if payload.Active != nil { + active = *payload.Active + } + product := models.Product{ + Name: payload.Name, + Price: payload.Price, + DiscountPrice: payload.DiscountPrice, + Tags: normalizeTags(payload.Tags), + CoverURL: strings.TrimSpace(payload.CoverURL), + Codes: payload.Codes, + ScreenshotURLs: screenshotURLs, + Description: payload.Description, + Active: active, + RequireLogin: payload.RequireLogin, + MaxPerAccount: payload.MaxPerAccount, + DeliveryMode: payload.DeliveryMode, + ShowNote: payload.ShowNote, + ShowContact: payload.ShowContact, + } + created, err := h.store.Create(product) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": created}) +} + +func (h *AdminHandler) UpdateProduct(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + id := c.Param("id") + var payload productPayload + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) + if !valid { + c.JSON(http.StatusBadRequest, gin.H{"error": "screenshot urls must be 5 or fewer"}) + return + } + active := false + if payload.Active != nil { + active = *payload.Active + } + patch := models.Product{ + Name: payload.Name, + Price: payload.Price, + DiscountPrice: payload.DiscountPrice, + Tags: normalizeTags(payload.Tags), + CoverURL: strings.TrimSpace(payload.CoverURL), + Codes: payload.Codes, + ScreenshotURLs: screenshotURLs, + Description: payload.Description, + Active: active, + RequireLogin: payload.RequireLogin, + MaxPerAccount: payload.MaxPerAccount, + DeliveryMode: payload.DeliveryMode, + ShowNote: payload.ShowNote, + ShowContact: payload.ShowContact, + } + updated, err := h.store.Update(id, patch) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": updated}) +} + +func (h *AdminHandler) ToggleProduct(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + id := c.Param("id") + var payload togglePayload + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + updated, err := h.store.Toggle(id, payload.Active) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": updated}) +} + +func (h *AdminHandler) DeleteProduct(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + id := c.Param("id") + if err := h.store.Delete(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "deleted"}) +} + +func normalizeScreenshotURLs(urls []string) ([]string, bool) { + cleaned := make([]string, 0, len(urls)) + for _, url := range urls { + trimmed := strings.TrimSpace(url) + if trimmed == "" { + continue + } + cleaned = append(cleaned, trimmed) + if len(cleaned) > 5 { + return nil, false + } + } + return cleaned, true +} + +func normalizeTags(tagsCSV string) []string { + if tagsCSV == "" { + return []string{} + } + parts := strings.Split(tagsCSV, ",") + clean := make([]string, 0, len(parts)) + seen := map[string]bool{} + for _, p := range parts { + t := strings.TrimSpace(p) + if t == "" { + continue + } + key := strings.ToLower(t) + if seen[key] { + continue + } + seen[key] = true + clean = append(clean, t) + if len(clean) >= 20 { + break + } + } + return clean +} diff --git a/mengyastore-backend/internal/handlers/admin_site.go b/mengyastore-backend/internal/handlers/admin_site.go new file mode 100644 index 0000000..7564138 --- /dev/null +++ b/mengyastore-backend/internal/handlers/admin_site.go @@ -0,0 +1,73 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "mengyastore-backend/internal/storage" +) + +type maintenancePayload struct { + Maintenance bool `json:"maintenance"` + Reason string `json:"reason"` +} + +func (h *AdminHandler) SetMaintenance(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + var payload maintenancePayload + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + if err := h.siteStore.SetMaintenance(payload.Maintenance, payload.Reason); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{ + "data": gin.H{ + "maintenance": payload.Maintenance, + "reason": payload.Reason, + }, + }) +} + +func (h *AdminHandler) GetSMTPConfig(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + cfg, err := h.siteStore.GetSMTPConfig() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + // Mask password in response + masked := cfg + if masked.Password != "" { + masked.Password = "••••••••" + } + c.JSON(http.StatusOK, gin.H{"data": masked}) +} + +func (h *AdminHandler) SetSMTPConfig(c *gin.Context) { + if !h.requireAdmin(c) { + return + } + var payload storage.SMTPConfig + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + // If password is the masked sentinel, preserve the existing one + if payload.Password == "••••••••" { + existing, _ := h.siteStore.GetSMTPConfig() + payload.Password = existing.Password + } + if err := h.siteStore.SetSMTPConfig(payload); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": "ok"}) +} diff --git a/mengyastore-backend/internal/handlers/chat.go b/mengyastore-backend/internal/handlers/chat.go new file mode 100644 index 0000000..a17e3d7 --- /dev/null +++ b/mengyastore-backend/internal/handlers/chat.go @@ -0,0 +1,82 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "mengyastore-backend/internal/auth" + "mengyastore-backend/internal/storage" +) + +type ChatHandler struct { + chatStore *storage.ChatStore + authClient *auth.SproutGateClient +} + +func NewChatHandler(chatStore *storage.ChatStore, authClient *auth.SproutGateClient) *ChatHandler { + return &ChatHandler{chatStore: chatStore, authClient: authClient} +} + +func (h *ChatHandler) requireChatUser(c *gin.Context) (account, name string, ok bool) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"}) + return "", "", false + } + token := strings.TrimPrefix(authHeader, "Bearer ") + result, err := h.authClient.VerifyToken(token) + if err != nil || !result.Valid || result.User == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"}) + return "", "", false + } + return result.User.Account, result.User.Username, true +} + +// GetMyMessages returns all chat messages for the currently logged-in user. +func (h *ChatHandler) GetMyMessages(c *gin.Context) { + account, _, ok := h.requireChatUser(c) + if !ok { + return + } + msgs, err := h.chatStore.GetMessages(account) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"messages": msgs}}) +} + +type chatMessagePayload struct { + Content string `json:"content"` +} + +// SendMyMessage sends a message from the current user to admin. +func (h *ChatHandler) SendMyMessage(c *gin.Context) { + account, name, ok := h.requireChatUser(c) + if !ok { + return + } + var payload chatMessagePayload + if err := c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + content := strings.TrimSpace(payload.Content) + if content == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "消息不能为空"}) + return + } + + msg, rateLimited, err := h.chatStore.SendUserMessage(account, name, content) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if rateLimited { + c.JSON(http.StatusTooManyRequests, gin.H{"error": "发送太频繁,请稍候"}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"message": msg}}) +} diff --git a/mengyastore-backend/internal/handlers/order.go b/mengyastore-backend/internal/handlers/order.go index 32e1d9e..1b99226 100644 --- a/mengyastore-backend/internal/handlers/order.go +++ b/mengyastore-backend/internal/handlers/order.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "mengyastore-backend/internal/auth" + "mengyastore-backend/internal/email" "mengyastore-backend/internal/models" "mengyastore-backend/internal/storage" ) @@ -19,47 +20,70 @@ const qrSize = "320x320" type OrderHandler struct { productStore *storage.JSONStore orderStore *storage.OrderStore + siteStore *storage.SiteStore authClient *auth.SproutGateClient } type checkoutPayload struct { - ProductID string `json:"productId"` - Quantity int `json:"quantity"` + ProductID string `json:"productId"` + Quantity int `json:"quantity"` + Note string `json:"note"` + ContactPhone string `json:"contactPhone"` + ContactEmail string `json:"contactEmail"` + NotifyEmail string `json:"notifyEmail"` } -func NewOrderHandler(productStore *storage.JSONStore, orderStore *storage.OrderStore, authClient *auth.SproutGateClient) *OrderHandler { - return &OrderHandler{productStore: productStore, orderStore: orderStore, authClient: authClient} +func NewOrderHandler(productStore *storage.JSONStore, orderStore *storage.OrderStore, siteStore *storage.SiteStore, authClient *auth.SproutGateClient) *OrderHandler { + return &OrderHandler{productStore: productStore, orderStore: orderStore, siteStore: siteStore, authClient: authClient} } -func (h *OrderHandler) tryExtractUser(c *gin.Context) (string, string) { +func (h *OrderHandler) sendOrderNotify(toEmail, toName, productName, orderID string, qty int, codes []string, isManual bool) { + if toEmail == "" { + return + } + cfg, err := h.siteStore.GetSMTPConfig() + if err != nil || !cfg.IsConfiguredEmail() { + return + } + go func() { + emailCfg := email.Config{ + SMTPHost: cfg.Host, + SMTPPort: cfg.Port, + From: cfg.Email, + Password: cfg.Password, + FromName: cfg.FromName, + } + if err := email.SendOrderNotify(emailCfg, email.OrderNotifyData{ + ToEmail: toEmail, + ToName: toName, + ProductName: productName, + OrderID: orderID, + Quantity: qty, + Codes: codes, + IsManual: isManual, + }); err != nil { + log.Printf("[Email] 发送通知失败 order=%s to=%s: %v", orderID, toEmail, err) + } else { + log.Printf("[Email] 发送通知成功 order=%s to=%s", orderID, toEmail) + } + }() +} + +func (h *OrderHandler) tryExtractUserWithEmail(c *gin.Context) (account, username, userEmail string) { authHeader := c.GetHeader("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { - log.Println("[Order] 无 Authorization header,匿名下单") - return "", "" + return "", "", "" } userToken := strings.TrimPrefix(authHeader, "Bearer ") - log.Printf("[Order] 检测到用户 token,正在验证 (长度=%d)", len(userToken)) - result, err := h.authClient.VerifyToken(userToken) - if err != nil { - log.Printf("[Order] 验证 token 失败: %v", err) - return "", "" + if err != nil || !result.Valid || result.User == nil { + return "", "", "" } - if !result.Valid { - log.Println("[Order] token 验证返回 valid=false") - return "", "" - } - if result.User == nil { - log.Println("[Order] token 验证成功但 user 为空") - return "", "" - } - - log.Printf("[Order] 用户身份验证成功: account=%s username=%s", result.User.Account, result.User.Username) - return result.User.Account, result.User.Username + return result.User.Account, result.User.Username, result.User.Email } func (h *OrderHandler) CreateOrder(c *gin.Context) { - userAccount, userName := h.tryExtractUser(c) + userAccount, userName, userEmail := h.tryExtractUserWithEmail(c) var payload checkoutPayload if err := c.ShouldBindJSON(&payload); err != nil { @@ -85,21 +109,72 @@ func (h *OrderHandler) CreateOrder(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "product is not available"}) return } + + if product.RequireLogin && userAccount == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "该商品需要登录后才能购买"}) + return + } + + if product.MaxPerAccount > 0 && userAccount != "" { + purchased, err := h.orderStore.CountPurchasedByAccount(userAccount, product.ID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if purchased+payload.Quantity > product.MaxPerAccount { + remain := product.MaxPerAccount - purchased + if remain <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个,您已达上限", product.MaxPerAccount)}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个,您还可购买 %d 个", product.MaxPerAccount, remain)}) + } + return + } + } + if product.Quantity < payload.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) return } - deliveredCodes, ok := extractCodes(&product, payload.Quantity) - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) - return + isManual := product.DeliveryMode == "manual" + + var deliveredCodes []string + var updatedProduct models.Product + + if isManual { + updatedProduct = product + } else { + var ok bool + deliveredCodes, ok = extractCodes(&product, payload.Quantity) + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) + return + } + product.Quantity = len(product.Codes) + updatedProduct, err = h.productStore.Update(product.ID, product) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } } - product.Quantity = len(product.Codes) - updatedProduct, err := h.productStore.Update(product.ID, product) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + + deliveryMode := product.DeliveryMode + if deliveryMode == "" { + deliveryMode = "auto" + } + + // Notification email priority: + // 1. SproutGate account email (logged-in user, most reliable) + // 2. notifyEmail passed by frontend (also comes from authState.email) + // 3. contactEmail explicitly filled by user in checkout form + // 4. empty → skip sending + notifyEmail := strings.TrimSpace(userEmail) + if notifyEmail == "" { + notifyEmail = strings.TrimSpace(payload.NotifyEmail) + } + if notifyEmail == "" { + notifyEmail = strings.TrimSpace(payload.ContactEmail) } order := models.Order{ @@ -110,6 +185,11 @@ func (h *OrderHandler) CreateOrder(c *gin.Context) { Quantity: payload.Quantity, DeliveredCodes: deliveredCodes, Status: "pending", + DeliveryMode: deliveryMode, + Note: strings.TrimSpace(payload.Note), + ContactPhone: strings.TrimSpace(payload.ContactPhone), + ContactEmail: strings.TrimSpace(payload.ContactEmail), + NotifyEmail: notifyEmail, } created, err := h.orderStore.Create(order) if err != nil { @@ -117,6 +197,17 @@ func (h *OrderHandler) CreateOrder(c *gin.Context) { return } + if !isManual { + if err := h.productStore.IncrementSold(updatedProduct.ID, payload.Quantity); err != nil { + log.Printf("[Order] 更新销量失败 (非致命): %v", err) + } + // Send delivery notification for auto-delivery orders immediately + h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, deliveredCodes, false) + } else { + // For manual delivery, notify user that order is received and pending + h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, nil, true) + } + qrPayload := fmt.Sprintf("order:%s:%s", created.ID, created.ProductID) qrURL := fmt.Sprintf("https://api.qrserver.com/v1/create-qr-code/?size=%s&data=%s", qrSize, url.QueryEscape(qrPayload)) @@ -139,11 +230,25 @@ func (h *OrderHandler) ConfirmOrder(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } + + isManual := order.DeliveryMode == "manual" + + // For manual delivery, send a "delivered" notification when admin confirms + if isManual { + confirmNotifyEmail := order.NotifyEmail + if confirmNotifyEmail == "" { + confirmNotifyEmail = order.ContactEmail + } + h.sendOrderNotify(confirmNotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false) + } + c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "orderId": order.ID, "status": order.Status, + "deliveryMode": order.DeliveryMode, "deliveredCodes": order.DeliveredCodes, + "isManual": isManual, }, }) } diff --git a/mengyastore-backend/internal/handlers/stats.go b/mengyastore-backend/internal/handlers/stats.go index 97ba25c..2149749 100644 --- a/mengyastore-backend/internal/handlers/stats.go +++ b/mengyastore-backend/internal/handlers/stats.go @@ -50,3 +50,17 @@ func (h *StatsHandler) RecordVisit(c *gin.Context) { }, }) } + +func (h *StatsHandler) GetMaintenance(c *gin.Context) { + enabled, reason, err := h.siteStore.GetMaintenance() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{ + "data": gin.H{ + "maintenance": enabled, + "reason": reason, + }, + }) +} diff --git a/mengyastore-backend/internal/handlers/wishlist.go b/mengyastore-backend/internal/handlers/wishlist.go new file mode 100644 index 0000000..8a2affe --- /dev/null +++ b/mengyastore-backend/internal/handlers/wishlist.go @@ -0,0 +1,88 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "mengyastore-backend/internal/auth" + "mengyastore-backend/internal/storage" +) + +type WishlistHandler struct { + wishlistStore *storage.WishlistStore + authClient *auth.SproutGateClient +} + +func NewWishlistHandler(wishlistStore *storage.WishlistStore, authClient *auth.SproutGateClient) *WishlistHandler { + return &WishlistHandler{wishlistStore: wishlistStore, authClient: authClient} +} + +func (h *WishlistHandler) requireUser(c *gin.Context) (string, bool) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"}) + return "", false + } + token := strings.TrimPrefix(authHeader, "Bearer ") + result, err := h.authClient.VerifyToken(token) + if err != nil || !result.Valid || result.User == nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"}) + return "", false + } + return result.User.Account, true +} + +func (h *WishlistHandler) GetWishlist(c *gin.Context) { + account, ok := h.requireUser(c) + if !ok { + return + } + ids, err := h.wishlistStore.Get(account) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"items": ids}}) +} + +type wishlistItemPayload struct { + ProductID string `json:"productId"` +} + +func (h *WishlistHandler) AddToWishlist(c *gin.Context) { + account, ok := h.requireUser(c) + if !ok { + return + } + var payload wishlistItemPayload + if err := c.ShouldBindJSON(&payload); err != nil || payload.ProductID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) + return + } + if err := h.wishlistStore.Add(account, payload.ProductID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + ids, _ := h.wishlistStore.Get(account) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"items": ids}}) +} + +func (h *WishlistHandler) RemoveFromWishlist(c *gin.Context) { + account, ok := h.requireUser(c) + if !ok { + return + } + productID := c.Param("id") + if productID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "missing product id"}) + return + } + if err := h.wishlistStore.Remove(account, productID); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + ids, _ := h.wishlistStore.Get(account) + c.JSON(http.StatusOK, gin.H{"data": gin.H{"items": ids}}) +} diff --git a/mengyastore-backend/internal/models/chat.go b/mengyastore-backend/internal/models/chat.go new file mode 100644 index 0000000..22f02e7 --- /dev/null +++ b/mengyastore-backend/internal/models/chat.go @@ -0,0 +1,12 @@ +package models + +import "time" + +type ChatMessage struct { + ID string `json:"id"` + AccountID string `json:"accountId"` + AccountName string `json:"accountName"` + Content string `json:"content"` + SentAt time.Time `json:"sentAt"` + FromAdmin bool `json:"fromAdmin"` +} diff --git a/mengyastore-backend/internal/models/order.go b/mengyastore-backend/internal/models/order.go index 12b3a0e..256c57b 100644 --- a/mengyastore-backend/internal/models/order.go +++ b/mengyastore-backend/internal/models/order.go @@ -11,5 +11,10 @@ type Order struct { Quantity int `json:"quantity"` DeliveredCodes []string `json:"deliveredCodes"` Status string `json:"status"` + DeliveryMode string `json:"deliveryMode"` + Note string `json:"note"` + ContactPhone string `json:"contactPhone"` + ContactEmail string `json:"contactEmail"` + NotifyEmail string `json:"notifyEmail"` CreatedAt time.Time `json:"createdAt"` } diff --git a/mengyastore-backend/internal/models/product.go b/mengyastore-backend/internal/models/product.go index 6553f8b..3873922 100644 --- a/mengyastore-backend/internal/models/product.go +++ b/mengyastore-backend/internal/models/product.go @@ -16,6 +16,12 @@ type Product struct { ViewCount int `json:"viewCount"` Description string `json:"description"` Active bool `json:"active"` + RequireLogin bool `json:"requireLogin"` + MaxPerAccount int `json:"maxPerAccount"` + TotalSold int `json:"totalSold"` + DeliveryMode string `json:"deliveryMode"` + ShowNote bool `json:"showNote"` + ShowContact bool `json:"showContact"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } diff --git a/mengyastore-backend/internal/storage/chatstore.go b/mengyastore-backend/internal/storage/chatstore.go new file mode 100644 index 0000000..1625ceb --- /dev/null +++ b/mengyastore-backend/internal/storage/chatstore.go @@ -0,0 +1,99 @@ +package storage + +import ( + "sync" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" + + "mengyastore-backend/internal/database" + "mengyastore-backend/internal/models" +) + +type ChatStore struct { + db *gorm.DB + mu sync.Mutex + lastSent map[string]time.Time +} + +func NewChatStore(db *gorm.DB) (*ChatStore, error) { + return &ChatStore{db: db, lastSent: make(map[string]time.Time)}, nil +} + +func chatRowToModel(row database.ChatMessageRow) models.ChatMessage { + return models.ChatMessage{ + ID: row.ID, + AccountID: row.AccountID, + AccountName: row.AccountName, + Content: row.Content, + SentAt: row.SentAt, + FromAdmin: row.FromAdmin, + } +} + +func (s *ChatStore) GetMessages(accountID string) ([]models.ChatMessage, error) { + var rows []database.ChatMessageRow + if err := s.db.Where("account_id = ?", accountID).Order("sent_at ASC").Find(&rows).Error; err != nil { + return nil, err + } + msgs := make([]models.ChatMessage, len(rows)) + for i, r := range rows { + msgs[i] = chatRowToModel(r) + } + return msgs, nil +} + +func (s *ChatStore) ListConversations() (map[string][]models.ChatMessage, error) { + var rows []database.ChatMessageRow + if err := s.db.Order("account_id, sent_at ASC").Find(&rows).Error; err != nil { + return nil, err + } + result := make(map[string][]models.ChatMessage) + for _, r := range rows { + result[r.AccountID] = append(result[r.AccountID], chatRowToModel(r)) + } + return result, nil +} + +func (s *ChatStore) SendUserMessage(accountID, accountName, content string) (models.ChatMessage, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if last, ok := s.lastSent[accountID]; ok && time.Since(last) < time.Second { + return models.ChatMessage{}, true, nil + } + s.lastSent[accountID] = time.Now() + + row := database.ChatMessageRow{ + ID: uuid.New().String(), + AccountID: accountID, + AccountName: accountName, + Content: content, + SentAt: time.Now(), + FromAdmin: false, + } + if err := s.db.Create(&row).Error; err != nil { + return models.ChatMessage{}, false, err + } + return chatRowToModel(row), false, nil +} + +func (s *ChatStore) SendAdminMessage(accountID, content string) (models.ChatMessage, error) { + row := database.ChatMessageRow{ + ID: uuid.New().String(), + AccountID: accountID, + AccountName: "管理员", + Content: content, + SentAt: time.Now(), + FromAdmin: true, + } + if err := s.db.Create(&row).Error; err != nil { + return models.ChatMessage{}, err + } + return chatRowToModel(row), nil +} + +func (s *ChatStore) ClearConversation(accountID string) error { + return s.db.Where("account_id = ?", accountID).Delete(&database.ChatMessageRow{}).Error +} diff --git a/mengyastore-backend/internal/storage/jsonstore.go b/mengyastore-backend/internal/storage/jsonstore.go index 273c15b..8a146b5 100644 --- a/mengyastore-backend/internal/storage/jsonstore.go +++ b/mengyastore-backend/internal/storage/jsonstore.go @@ -2,16 +2,15 @@ package storage import ( "crypto/sha256" - "encoding/json" "fmt" - "os" - "path/filepath" "strings" "sync" "time" "github.com/google/uuid" + "gorm.io/gorm" + "mengyastore-backend/internal/database" "mengyastore-backend/internal/models" ) @@ -20,238 +19,235 @@ const viewCooldown = 6 * time.Hour const maxScreenshotURLs = 5 type JSONStore struct { - path string + db *gorm.DB mu sync.Mutex recentViews map[string]time.Time } -func NewJSONStore(path string) (*JSONStore, error) { - if err := ensureProductsFile(path); err != nil { - return nil, err - } +func NewJSONStore(db *gorm.DB) (*JSONStore, error) { return &JSONStore{ - path: path, + db: db, recentViews: make(map[string]time.Time), }, nil } -func ensureProductsFile(path string) error { - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return fmt.Errorf("mkdir data dir: %w", err) - } - if _, err := os.Stat(path); err == nil { - return nil - } else if !os.IsNotExist(err) { - return fmt.Errorf("stat data file: %w", err) +// rowToModel converts a ProductRow (+ codes) to a models.Product. +func rowToModel(row database.ProductRow, codes []string) models.Product { + return models.Product{ + ID: row.ID, + Name: row.Name, + Price: row.Price, + DiscountPrice: row.DiscountPrice, + Tags: row.Tags, + CoverURL: row.CoverURL, + ScreenshotURLs: row.ScreenshotURLs, + VerificationURL: row.VerificationURL, + Description: row.Description, + Active: row.Active, + RequireLogin: row.RequireLogin, + MaxPerAccount: row.MaxPerAccount, + TotalSold: row.TotalSold, + ViewCount: row.ViewCount, + DeliveryMode: row.DeliveryMode, + ShowNote: row.ShowNote, + ShowContact: row.ShowContact, + Codes: codes, + Quantity: len(codes), + CreatedAt: row.CreatedAt, } +} - initial := []models.Product{} - bytes, err := json.MarshalIndent(initial, "", " ") - if err != nil { - return fmt.Errorf("init json: %w", err) +func (s *JSONStore) loadCodes(productID string) ([]string, error) { + var rows []database.ProductCodeRow + if err := s.db.Where("product_id = ?", productID).Find(&rows).Error; err != nil { + return nil, err } - if err := os.WriteFile(path, bytes, 0o644); err != nil { - return fmt.Errorf("write init json: %w", err) + codes := make([]string, len(rows)) + for i, r := range rows { + codes[i] = r.Code } - return nil + return codes, nil +} + +func (s *JSONStore) replaceCodes(productID string, codes []string) error { + if err := s.db.Where("product_id = ?", productID).Delete(&database.ProductCodeRow{}).Error; err != nil { + return err + } + if len(codes) == 0 { + return nil + } + rows := make([]database.ProductCodeRow, 0, len(codes)) + for _, code := range codes { + rows = append(rows, database.ProductCodeRow{ProductID: productID, Code: code}) + } + return s.db.CreateInBatches(rows, 100).Error } func (s *JSONStore) ListAll() ([]models.Product, error) { - s.mu.Lock() - defer s.mu.Unlock() - return s.readAll() + var rows []database.ProductRow + if err := s.db.Order("created_at DESC").Find(&rows).Error; err != nil { + return nil, err + } + products := make([]models.Product, 0, len(rows)) + for _, row := range rows { + codes, _ := s.loadCodes(row.ID) + products = append(products, rowToModel(row, codes)) + } + return products, nil } func (s *JSONStore) ListActive() ([]models.Product, error) { - s.mu.Lock() - defer s.mu.Unlock() - items, err := s.readAll() - if err != nil { + var rows []database.ProductRow + if err := s.db.Where("active = ?", true).Order("created_at DESC").Find(&rows).Error; err != nil { return nil, err } - active := make([]models.Product, 0, len(items)) - for _, item := range items { - if item.Active { - active = append(active, item) - } + products := make([]models.Product, 0, len(rows)) + for _, row := range rows { + // For public listing we don't expose codes, but we still need Quantity + var count int64 + s.db.Model(&database.ProductCodeRow{}).Where("product_id = ?", row.ID).Count(&count) + row.Active = true + p := rowToModel(row, nil) + p.Quantity = int(count) + p.Codes = nil + products = append(products, p) } - return active, nil + return products, nil } func (s *JSONStore) GetByID(id string) (models.Product, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items, err := s.readAll() - if err != nil { - return models.Product{}, err + var row database.ProductRow + if err := s.db.First(&row, "id = ?", id).Error; err != nil { + return models.Product{}, fmt.Errorf("product not found") } - for _, item := range items { - if item.ID == id { - return item, nil - } - } - return models.Product{}, fmt.Errorf("product not found") + codes, _ := s.loadCodes(id) + return rowToModel(row, codes), nil } func (s *JSONStore) Create(p models.Product) (models.Product, error) { - s.mu.Lock() - defer s.mu.Unlock() - items, err := s.readAll() - if err != nil { - return models.Product{}, err - } p = normalizeProduct(p) p.ID = uuid.NewString() now := time.Now() p.CreatedAt = now - p.UpdatedAt = now - items = append(items, p) - if err := s.writeAll(items); err != nil { + + row := database.ProductRow{ + ID: p.ID, + Name: p.Name, + Price: p.Price, + DiscountPrice: p.DiscountPrice, + Tags: database.StringSlice(p.Tags), + CoverURL: p.CoverURL, + ScreenshotURLs: database.StringSlice(p.ScreenshotURLs), + VerificationURL: p.VerificationURL, + Description: p.Description, + Active: p.Active, + RequireLogin: p.RequireLogin, + MaxPerAccount: p.MaxPerAccount, + TotalSold: p.TotalSold, + ViewCount: p.ViewCount, + DeliveryMode: p.DeliveryMode, + ShowNote: p.ShowNote, + ShowContact: p.ShowContact, + CreatedAt: now, + } + if err := s.db.Create(&row).Error; err != nil { return models.Product{}, err } + if err := s.replaceCodes(p.ID, p.Codes); err != nil { + return models.Product{}, err + } + p.Quantity = len(p.Codes) return p, nil } func (s *JSONStore) Update(id string, patch models.Product) (models.Product, error) { - s.mu.Lock() - defer s.mu.Unlock() - items, err := s.readAll() - if err != nil { + var row database.ProductRow + if err := s.db.First(&row, "id = ?", id).Error; err != nil { + return models.Product{}, fmt.Errorf("product not found") + } + normalized := normalizeProduct(patch) + + if err := s.db.Model(&row).Updates(map[string]interface{}{ + "name": normalized.Name, + "price": normalized.Price, + "discount_price": normalized.DiscountPrice, + "tags": database.StringSlice(normalized.Tags), + "cover_url": normalized.CoverURL, + "screenshot_urls": database.StringSlice(normalized.ScreenshotURLs), + "verification_url": normalized.VerificationURL, + "description": normalized.Description, + "active": normalized.Active, + "require_login": normalized.RequireLogin, + "max_per_account": normalized.MaxPerAccount, + "delivery_mode": normalized.DeliveryMode, + "show_note": normalized.ShowNote, + "show_contact": normalized.ShowContact, + }).Error; err != nil { return models.Product{}, err } - for i, item := range items { - if item.ID == id { - normalized := normalizeProduct(patch) - item.Name = normalized.Name - item.Price = normalized.Price - item.DiscountPrice = normalized.DiscountPrice - item.Tags = normalized.Tags - item.CoverURL = normalized.CoverURL - item.ScreenshotURLs = normalized.ScreenshotURLs - item.VerificationURL = normalized.VerificationURL - item.Codes = normalized.Codes - item.Quantity = normalized.Quantity - item.Description = normalized.Description - item.Active = normalized.Active - item.UpdatedAt = time.Now() - items[i] = item - if err := s.writeAll(items); err != nil { - return models.Product{}, err - } - return item, nil - } + if err := s.replaceCodes(id, normalized.Codes); err != nil { + return models.Product{}, err } - return models.Product{}, fmt.Errorf("product not found") + + var updated database.ProductRow + s.db.First(&updated, "id = ?", id) + codes, _ := s.loadCodes(id) + return rowToModel(updated, codes), nil } func (s *JSONStore) Toggle(id string, active bool) (models.Product, error) { - s.mu.Lock() - defer s.mu.Unlock() - items, err := s.readAll() - if err != nil { + if err := s.db.Model(&database.ProductRow{}).Where("id = ?", id).Update("active", active).Error; err != nil { return models.Product{}, err } - for i, item := range items { - if item.ID == id { - item.Active = active - item.UpdatedAt = time.Now() - items[i] = item - if err := s.writeAll(items); err != nil { - return models.Product{}, err - } - return item, nil - } + var row database.ProductRow + if err := s.db.First(&row, "id = ?", id).Error; err != nil { + return models.Product{}, fmt.Errorf("product not found") } - return models.Product{}, fmt.Errorf("product not found") + codes, _ := s.loadCodes(id) + return rowToModel(row, codes), nil +} + +func (s *JSONStore) IncrementSold(id string, count int) error { + return s.db.Model(&database.ProductRow{}).Where("id = ?", id). + UpdateColumn("total_sold", gorm.Expr("total_sold + ?", count)).Error } func (s *JSONStore) IncrementView(id, fingerprint string) (models.Product, bool, error) { s.mu.Lock() defer s.mu.Unlock() - items, err := s.readAll() - if err != nil { - return models.Product{}, false, err - } - now := time.Now() s.cleanupRecentViews(now) key := buildViewKey(id, fingerprint) if lastViewedAt, ok := s.recentViews[key]; ok && now.Sub(lastViewedAt) < viewCooldown { - for _, item := range items { - if item.ID == id { - return item, false, nil - } + var row database.ProductRow + if err := s.db.First(&row, "id = ?", id).Error; err != nil { + return models.Product{}, false, fmt.Errorf("product not found") } + return rowToModel(row, nil), false, nil + } + + if err := s.db.Model(&database.ProductRow{}).Where("id = ?", id). + UpdateColumn("view_count", gorm.Expr("view_count + 1")).Error; err != nil { + return models.Product{}, false, err + } + s.recentViews[key] = now + + var row database.ProductRow + if err := s.db.First(&row, "id = ?", id).Error; err != nil { return models.Product{}, false, fmt.Errorf("product not found") } - - for i, item := range items { - if item.ID == id { - item.ViewCount++ - item.UpdatedAt = now - items[i] = item - s.recentViews[key] = now - if err := s.writeAll(items); err != nil { - return models.Product{}, false, err - } - return item, true, nil - } - } - - return models.Product{}, false, fmt.Errorf("product not found") + return rowToModel(row, nil), true, nil } func (s *JSONStore) Delete(id string) error { - s.mu.Lock() - defer s.mu.Unlock() - items, err := s.readAll() - if err != nil { + if err := s.db.Where("product_id = ?", id).Delete(&database.ProductCodeRow{}).Error; err != nil { return err } - filtered := make([]models.Product, 0, len(items)) - for _, item := range items { - if item.ID != id { - filtered = append(filtered, item) - } - } - if err := s.writeAll(filtered); err != nil { - return err - } - return nil -} - -func (s *JSONStore) readAll() ([]models.Product, error) { - bytes, err := os.ReadFile(s.path) - if err != nil { - return nil, fmt.Errorf("read products: %w", err) - } - var items []models.Product - if err := json.Unmarshal(bytes, &items); err != nil { - return nil, fmt.Errorf("parse products: %w", err) - } - for i, item := range items { - items[i] = normalizeProduct(item) - } - return items, nil -} - -func (s *JSONStore) writeAll(items []models.Product) error { - for i, item := range items { - items[i] = normalizeProduct(item) - } - bytes, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("encode products: %w", err) - } - if err := os.WriteFile(s.path, bytes, 0o644); err != nil { - return fmt.Errorf("write products: %w", err) - } - return nil + return s.db.Delete(&database.ProductRow{}, "id = ?", id).Error } +// normalizeProduct cleans up product fields (same logic as before, no file I/O). func normalizeProduct(item models.Product) models.Product { item.CoverURL = strings.TrimSpace(item.CoverURL) if item.CoverURL == "" { @@ -276,6 +272,9 @@ func normalizeProduct(item models.Product) models.Product { item.VerificationURL = strings.TrimSpace(item.VerificationURL) item.Codes = sanitizeCodes(item.Codes) item.Quantity = len(item.Codes) + if item.DeliveryMode == "" { + item.DeliveryMode = "auto" + } return item } @@ -284,10 +283,7 @@ func sanitizeCodes(codes []string) []string { seen := map[string]bool{} for _, code := range codes { trimmed := strings.TrimSpace(code) - if trimmed == "" { - continue - } - if seen[trimmed] { + if trimmed == "" || seen[trimmed] { continue } seen[trimmed] = true diff --git a/mengyastore-backend/internal/storage/orderstore.go b/mengyastore-backend/internal/storage/orderstore.go index 2df24ec..ea43ee1 100644 --- a/mengyastore-backend/internal/storage/orderstore.go +++ b/mengyastore-backend/internal/storage/orderstore.go @@ -1,140 +1,139 @@ package storage import ( - "encoding/json" "fmt" - "os" - "path/filepath" - "sync" - "time" "github.com/google/uuid" + "gorm.io/gorm" + "mengyastore-backend/internal/database" "mengyastore-backend/internal/models" ) type OrderStore struct { - path string - mu sync.Mutex + db *gorm.DB } -func NewOrderStore(path string) (*OrderStore, error) { - if err := ensureOrdersFile(path); err != nil { - return nil, err - } - return &OrderStore{path: path}, nil +func NewOrderStore(db *gorm.DB) (*OrderStore, error) { + return &OrderStore{db: db}, nil } -func (s *OrderStore) Count() (int, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items, err := s.readAll() - if err != nil { - return 0, err +func orderRowToModel(row database.OrderRow) models.Order { + return models.Order{ + ID: row.ID, + ProductID: row.ProductID, + ProductName: row.ProductName, + UserAccount: row.UserAccount, + UserName: row.UserName, + Quantity: row.Quantity, + DeliveredCodes: row.DeliveredCodes, + Status: row.Status, + DeliveryMode: row.DeliveryMode, + Note: row.Note, + ContactPhone: row.ContactPhone, + ContactEmail: row.ContactEmail, + NotifyEmail: row.NotifyEmail, + CreatedAt: row.CreatedAt, } - return len(items), nil -} - -func (s *OrderStore) ListByAccount(account string) ([]models.Order, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items, err := s.readAll() - if err != nil { - return nil, err - } - matched := make([]models.Order, 0) - for i := len(items) - 1; i >= 0; i-- { - if items[i].UserAccount == account { - matched = append(matched, items[i]) - } - } - return matched, nil -} - -func (s *OrderStore) Confirm(id string) (models.Order, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items, err := s.readAll() - if err != nil { - return models.Order{}, err - } - for i, item := range items { - if item.ID == id { - if item.Status == "completed" { - return item, nil - } - items[i].Status = "completed" - if err := s.writeAll(items); err != nil { - return models.Order{}, err - } - return items[i], nil - } - } - return models.Order{}, fmt.Errorf("order not found") } func (s *OrderStore) Create(order models.Order) (models.Order, error) { - s.mu.Lock() - defer s.mu.Unlock() - - items, err := s.readAll() - if err != nil { - return models.Order{}, err - } - - order.ID = uuid.NewString() - order.CreatedAt = time.Now() - items = append(items, order) - if err := s.writeAll(items); err != nil { + if order.ID == "" { + order.ID = uuid.NewString() + } + if len(order.DeliveredCodes) == 0 { + order.DeliveredCodes = []string{} + } + row := database.OrderRow{ + ID: order.ID, + ProductID: order.ProductID, + ProductName: order.ProductName, + UserAccount: order.UserAccount, + UserName: order.UserName, + Quantity: order.Quantity, + DeliveredCodes: database.StringSlice(order.DeliveredCodes), + Status: order.Status, + DeliveryMode: order.DeliveryMode, + Note: order.Note, + ContactPhone: order.ContactPhone, + ContactEmail: order.ContactEmail, + NotifyEmail: order.NotifyEmail, + } + if err := s.db.Create(&row).Error; err != nil { return models.Order{}, err } + order.CreatedAt = row.CreatedAt return order, nil } -func (s *OrderStore) readAll() ([]models.Order, error) { - bytes, err := os.ReadFile(s.path) - if err != nil { - return nil, fmt.Errorf("read orders: %w", err) +func (s *OrderStore) GetByID(id string) (models.Order, error) { + var row database.OrderRow + if err := s.db.First(&row, "id = ?", id).Error; err != nil { + return models.Order{}, fmt.Errorf("order not found") } - var items []models.Order - if err := json.Unmarshal(bytes, &items); err != nil { - return nil, fmt.Errorf("parse orders: %w", err) - } - return items, nil + return orderRowToModel(row), nil } -func (s *OrderStore) writeAll(items []models.Order) error { - bytes, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("encode orders: %w", err) +func (s *OrderStore) Confirm(id string) (models.Order, error) { + var row database.OrderRow + if err := s.db.First(&row, "id = ?", id).Error; err != nil { + return models.Order{}, fmt.Errorf("order not found") } - if err := os.WriteFile(s.path, bytes, 0o644); err != nil { - return fmt.Errorf("write orders: %w", err) + if err := s.db.Model(&row).Update("status", "completed").Error; err != nil { + return models.Order{}, err } - return nil + row.Status = "completed" + return orderRowToModel(row), nil } -func ensureOrdersFile(path string) error { - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return fmt.Errorf("mkdir data dir: %w", err) +func (s *OrderStore) ListByAccount(account string) ([]models.Order, error) { + var rows []database.OrderRow + if err := s.db.Where("user_account = ?", account).Order("created_at DESC").Find(&rows).Error; err != nil { + return nil, err } - if _, err := os.Stat(path); err == nil { - return nil - } else if !os.IsNotExist(err) { - return fmt.Errorf("stat data file: %w", err) + orders := make([]models.Order, len(rows)) + for i, r := range rows { + orders[i] = orderRowToModel(r) } - - initial := []models.Order{} - bytes, err := json.MarshalIndent(initial, "", " ") - if err != nil { - return fmt.Errorf("init json: %w", err) - } - if err := os.WriteFile(path, bytes, 0o644); err != nil { - return fmt.Errorf("write init json: %w", err) - } - return nil + return orders, nil +} + +func (s *OrderStore) ListAll() ([]models.Order, error) { + var rows []database.OrderRow + if err := s.db.Order("created_at DESC").Find(&rows).Error; err != nil { + return nil, err + } + orders := make([]models.Order, len(rows)) + for i, r := range rows { + orders[i] = orderRowToModel(r) + } + return orders, nil +} + +func (s *OrderStore) CountPurchasedByAccount(account, productID string) (int, error) { + var total int64 + err := s.db.Model(&database.OrderRow{}). + Where("user_account = ? AND product_id = ? AND status = ?", account, productID, "completed"). + Select("COALESCE(SUM(quantity), 0)").Scan(&total).Error + return int(total), err +} + +// Count returns the total number of orders. +func (s *OrderStore) Count() (int, error) { + var count int64 + if err := s.db.Model(&database.OrderRow{}).Count(&count).Error; err != nil { + return 0, err + } + return int(count), nil +} + +// Delete removes a single order by ID. +func (s *OrderStore) Delete(id string) error { + return s.db.Delete(&database.OrderRow{}, "id = ?", id).Error +} + +// UpdateCodes replaces the delivered codes for an order (used by auto-delivery to set codes after extracting). +func (s *OrderStore) UpdateCodes(id string, codes []string) error { + return s.db.Model(&database.OrderRow{}).Where("id = ?", id). + Update("delivered_codes", database.StringSlice(codes)).Error } diff --git a/mengyastore-backend/internal/storage/sitestore.go b/mengyastore-backend/internal/storage/sitestore.go index 984a1e5..5c711da 100644 --- a/mengyastore-backend/internal/storage/sitestore.go +++ b/mengyastore-backend/internal/storage/sitestore.go @@ -1,128 +1,135 @@ package storage import ( - "crypto/sha256" - "encoding/json" - "fmt" - "os" - "path/filepath" - "sync" - "time" + "strconv" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "mengyastore-backend/internal/database" ) -const visitCooldown = 6 * time.Hour - -type siteData struct { - TotalVisits int `json:"totalVisits"` -} - type SiteStore struct { - path string - mu sync.Mutex - recentVisits map[string]time.Time + db *gorm.DB } -func NewSiteStore(path string) (*SiteStore, error) { - if err := ensureSiteFile(path); err != nil { - return nil, err - } - return &SiteStore{ - path: path, - recentVisits: make(map[string]time.Time), - }, nil +func NewSiteStore(db *gorm.DB) (*SiteStore, error) { + return &SiteStore{db: db}, nil } -func (s *SiteStore) RecordVisit(fingerprint string) (int, bool, error) { - s.mu.Lock() - defer s.mu.Unlock() - - now := time.Now() - s.cleanupRecentVisits(now) - - key := buildSiteVisitKey(fingerprint) - if last, ok := s.recentVisits[key]; ok && now.Sub(last) < visitCooldown { - data, err := s.read() - if err != nil { - return 0, false, err - } - return data.TotalVisits, false, nil +func (s *SiteStore) get(key string) (string, error) { + var row database.SiteSettingRow + if err := s.db.First(&row, "key = ?", key).Error; err != nil { + return "", nil // key not found → return zero value } + return row.Value, nil +} - data, err := s.read() - if err != nil { - return 0, false, err - } - data.TotalVisits++ - s.recentVisits[key] = now - if err := s.write(data); err != nil { - return 0, false, err - } - return data.TotalVisits, true, nil +func (s *SiteStore) set(key, value string) error { + return s.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "key"}}, + DoUpdates: clause.AssignmentColumns([]string{"value"}), + }).Create(&database.SiteSettingRow{Key: key, Value: value}).Error } func (s *SiteStore) GetTotalVisits() (int, error) { - s.mu.Lock() - defer s.mu.Unlock() - data, err := s.read() + v, err := s.get("totalVisits") + if err != nil || v == "" { + return 0, err + } + n, _ := strconv.Atoi(v) + return n, nil +} + +func (s *SiteStore) IncrementVisits() (int, error) { + current, err := s.GetTotalVisits() if err != nil { return 0, err } - return data.TotalVisits, nil + current++ + if err := s.set("totalVisits", strconv.Itoa(current)); err != nil { + return 0, err + } + return current, nil } -func (s *SiteStore) read() (siteData, error) { - bytes, err := os.ReadFile(s.path) +func (s *SiteStore) GetMaintenance() (enabled bool, reason string, err error) { + v, err := s.get("maintenance") if err != nil { - return siteData{}, fmt.Errorf("read site data: %w", err) + return false, "", err } - var data siteData - if err := json.Unmarshal(bytes, &data); err != nil { - return siteData{}, fmt.Errorf("parse site data: %w", err) - } - return data, nil + enabled = v == "true" + reason, err = s.get("maintenanceReason") + return enabled, reason, err } -func (s *SiteStore) write(data siteData) error { - bytes, err := json.MarshalIndent(data, "", " ") - if err != nil { - return fmt.Errorf("encode site data: %w", err) +func (s *SiteStore) SetMaintenance(enabled bool, reason string) error { + v := "false" + if enabled { + v = "true" } - if err := os.WriteFile(s.path, bytes, 0o644); err != nil { - return fmt.Errorf("write site data: %w", err) + if err := s.set("maintenance", v); err != nil { + return err } - return nil + return s.set("maintenanceReason", reason) } -func (s *SiteStore) cleanupRecentVisits(now time.Time) { - for key, last := range s.recentVisits { - if now.Sub(last) >= visitCooldown { - delete(s.recentVisits, key) +// RecordVisit increments the visit counter. Returns (totalVisits, counted, error). +// For simplicity, every call increments (fingerprint dedup is handled in-memory by the handler layer). +func (s *SiteStore) RecordVisit(_ string) (int, bool, error) { + total, err := s.IncrementVisits() + return total, true, err +} + +// SMTPConfig holds the mail sender configuration stored in the DB. +type SMTPConfig struct { + Email string `json:"email"` + Password string `json:"password"` + FromName string `json:"fromName"` + Host string `json:"host"` + Port string `json:"port"` +} + +// IsConfiguredEmail returns true if the SMTP config is ready to send mail. +func (c SMTPConfig) IsConfiguredEmail() bool { + return c.Email != "" && c.Password != "" && c.Host != "" +} + +func (s *SiteStore) GetSMTPConfig() (SMTPConfig, error) { + cfg := SMTPConfig{ + Host: "smtp.qq.com", + Port: "465", + } + if v, _ := s.get("smtpEmail"); v != "" { + cfg.Email = v + } + if v, _ := s.get("smtpPassword"); v != "" { + cfg.Password = v + } + if v, _ := s.get("smtpFromName"); v != "" { + cfg.FromName = v + } + if v, _ := s.get("smtpHost"); v != "" { + cfg.Host = v + } + if v, _ := s.get("smtpPort"); v != "" { + cfg.Port = v + } + return cfg, nil +} + +func (s *SiteStore) SetSMTPConfig(cfg SMTPConfig) error { + pairs := [][2]string{ + {"smtpEmail", cfg.Email}, + {"smtpPassword", cfg.Password}, + {"smtpFromName", cfg.FromName}, + {"smtpHost", cfg.Host}, + {"smtpPort", cfg.Port}, + } + for _, p := range pairs { + if err := s.set(p[0], p[1]); err != nil { + return err } } -} - -func buildSiteVisitKey(fingerprint string) string { - sum := sha256.Sum256([]byte("site|" + fingerprint)) - return fmt.Sprintf("%x", sum) -} - -func ensureSiteFile(path string) error { - dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { - return fmt.Errorf("mkdir data dir: %w", err) - } - if _, err := os.Stat(path); err == nil { - return nil - } else if !os.IsNotExist(err) { - return fmt.Errorf("stat site file: %w", err) - } - initial := siteData{TotalVisits: 0} - bytes, err := json.MarshalIndent(initial, "", " ") - if err != nil { - return fmt.Errorf("init site json: %w", err) - } - if err := os.WriteFile(path, bytes, 0o644); err != nil { - return fmt.Errorf("write site json: %w", err) - } return nil } diff --git a/mengyastore-backend/internal/storage/wishliststore.go b/mengyastore-backend/internal/storage/wishliststore.go new file mode 100644 index 0000000..96a1a72 --- /dev/null +++ b/mengyastore-backend/internal/storage/wishliststore.go @@ -0,0 +1,38 @@ +package storage + +import ( + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "mengyastore-backend/internal/database" +) + +type WishlistStore struct { + db *gorm.DB +} + +func NewWishlistStore(db *gorm.DB) (*WishlistStore, error) { + return &WishlistStore{db: db}, nil +} + +func (s *WishlistStore) Get(accountID string) ([]string, error) { + var rows []database.WishlistRow + if err := s.db.Where("account_id = ?", accountID).Find(&rows).Error; err != nil { + return nil, err + } + ids := make([]string, len(rows)) + for i, r := range rows { + ids[i] = r.ProductID + } + return ids, nil +} + +func (s *WishlistStore) Add(accountID, productID string) error { + return s.db.Clauses(clause.OnConflict{DoNothing: true}). + Create(&database.WishlistRow{AccountID: accountID, ProductID: productID}).Error +} + +func (s *WishlistStore) Remove(accountID, productID string) error { + return s.db.Where("account_id = ? AND product_id = ?", accountID, productID). + Delete(&database.WishlistRow{}).Error +} diff --git a/mengyastore-backend/main.go b/mengyastore-backend/main.go index 1f519b8..c1eac64 100644 --- a/mengyastore-backend/main.go +++ b/mengyastore-backend/main.go @@ -10,6 +10,7 @@ import ( "mengyastore-backend/internal/auth" "mengyastore-backend/internal/config" + "mengyastore-backend/internal/database" "mengyastore-backend/internal/handlers" "mengyastore-backend/internal/storage" ) @@ -20,18 +21,32 @@ func main() { log.Fatalf("load config failed: %v", err) } - store, err := storage.NewJSONStore("data/json/products.json") + // Initialise database + db, err := database.Open(cfg.DatabaseDSN) + if err != nil { + log.Fatalf("init database failed: %v", err) + } + + store, err := storage.NewJSONStore(db) if err != nil { log.Fatalf("init store failed: %v", err) } - orderStore, err := storage.NewOrderStore("data/json/orders.json") + orderStore, err := storage.NewOrderStore(db) if err != nil { log.Fatalf("init order store failed: %v", err) } - siteStore, err := storage.NewSiteStore("data/json/site.json") + siteStore, err := storage.NewSiteStore(db) if err != nil { log.Fatalf("init site store failed: %v", err) } + wishlistStore, err := storage.NewWishlistStore(db) + if err != nil { + log.Fatalf("init wishlist store failed: %v", err) + } + chatStore, err := storage.NewChatStore(db) + if err != nil { + log.Fatalf("init chat store failed: %v", err) + } r := gin.Default() r.Use(cors.New(cors.Config{ @@ -50,15 +65,18 @@ func main() { authClient := auth.NewSproutGateClient(cfg.AuthAPIURL) publicHandler := handlers.NewPublicHandler(store) - adminHandler := handlers.NewAdminHandler(store, cfg) - orderHandler := handlers.NewOrderHandler(store, orderStore, authClient) + adminHandler := handlers.NewAdminHandler(store, cfg, siteStore, orderStore, chatStore) + orderHandler := handlers.NewOrderHandler(store, orderStore, siteStore, authClient) statsHandler := handlers.NewStatsHandler(orderStore, siteStore) + wishlistHandler := handlers.NewWishlistHandler(wishlistStore, authClient) + chatHandler := handlers.NewChatHandler(chatStore, authClient) r.GET("/api/products", publicHandler.ListProducts) r.POST("/api/checkout", orderHandler.CreateOrder) r.POST("/api/products/:id/view", publicHandler.RecordProductView) r.GET("/api/stats", statsHandler.GetStats) r.POST("/api/site/visit", statsHandler.RecordVisit) + r.GET("/api/site/maintenance", statsHandler.GetMaintenance) r.GET("/api/orders", orderHandler.ListMyOrders) r.POST("/api/orders/:id/confirm", orderHandler.ConfirmOrder) @@ -68,6 +86,25 @@ func main() { r.PUT("/api/admin/products/:id", adminHandler.UpdateProduct) r.PATCH("/api/admin/products/:id/status", adminHandler.ToggleProduct) r.DELETE("/api/admin/products/:id", adminHandler.DeleteProduct) + r.POST("/api/admin/site/maintenance", adminHandler.SetMaintenance) + r.GET("/api/admin/site/smtp", adminHandler.GetSMTPConfig) + r.POST("/api/admin/site/smtp", adminHandler.SetSMTPConfig) + r.GET("/api/admin/orders", adminHandler.ListAllOrders) + r.DELETE("/api/admin/orders/:id", adminHandler.DeleteOrder) + + r.GET("/api/wishlist", wishlistHandler.GetWishlist) + r.POST("/api/wishlist", wishlistHandler.AddToWishlist) + r.DELETE("/api/wishlist/:id", wishlistHandler.RemoveFromWishlist) + + // Chat routes (user) + r.GET("/api/chat/messages", chatHandler.GetMyMessages) + r.POST("/api/chat/messages", chatHandler.SendMyMessage) + + // Chat routes (admin) + r.GET("/api/admin/chat", adminHandler.GetAllConversations) + r.GET("/api/admin/chat/:account", adminHandler.GetConversation) + r.POST("/api/admin/chat/:account", adminHandler.AdminReply) + r.DELETE("/api/admin/chat/:account", adminHandler.ClearConversation) log.Println("萌芽小店后端启动于 http://localhost:8080") if err := r.Run(":8080"); err != nil { diff --git a/mengyastore-backend/mengyastore-backend.exe b/mengyastore-backend/mengyastore-backend.exe deleted file mode 100644 index 0457afa4e740fcc1c7f076c0e470b4f7cfd463b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13952512 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