feat(deploy): production deployment setup and fixes

- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
yeuimu
2026-08-26 14:23:09 +08:00
parent be0b90e68f
commit 6c61a4e871
982 changed files with 74156 additions and 179393 deletions
+18 -18
View File
@@ -1,19 +1,19 @@
import request from './request'
import type { LoginRequest, LoginResponse, User } from '@/types'
export const authApi = {
// Login
login: (data: LoginRequest) => {
return request.post<any, LoginResponse>('/auth/login', data)
},
// Get current user
getCurrentUser: () => {
return request.get<any, User>('/auth/me')
},
// Logout
logout: () => {
return request.post('/auth/logout')
},
import request from './request'
import type { LoginRequest, LoginResponse, User } from '@/types'
export const authApi = {
// Login
login: (data: LoginRequest) => {
return request.post<any, LoginResponse>('/auth/login', data)
},
// Get current user
getCurrentUser: () => {
return request.get<any, User>('/auth/me')
},
// Logout
logout: () => {
return request.post('/auth/logout')
},
}
+40 -40
View File
@@ -1,41 +1,41 @@
import request from './request'
import type {
Category,
CategoryTree,
CreateCategoryRequest,
UpdateCategoryRequest,
CategoryFilter,
PaginatedResult,
} from '@/types'
export const categoriesApi = {
// Get categories list
getCategoriesList: (params: CategoryFilter) => {
return request.get<any, PaginatedResult<Category>>('/categories', { params })
},
// Get category tree
getCategoryTree: () => {
return request.get<any, CategoryTree[]>('/categories')
},
// Get category by id
getCategoryById: (id: string) => {
return request.get<any, Category>(`/categories/${id}`)
},
// Create category
createCategory: (data: CreateCategoryRequest) => {
return request.post<any, Category>('/categories', data)
},
// Update category
updateCategory: (id: string, data: UpdateCategoryRequest) => {
return request.patch<any, Category>(`/categories/${id}`, data)
},
// Delete category
deleteCategory: (id: string) => {
return request.delete(`/categories/${id}`)
},
import request from './request'
import type {
Category,
CategoryTree,
CreateCategoryRequest,
UpdateCategoryRequest,
CategoryFilter,
PaginatedResult,
} from '@/types'
export const categoriesApi = {
// Get categories list
getCategoriesList: (params: CategoryFilter) => {
return request.get<any, PaginatedResult<Category>>('/categories', { params })
},
// Get category tree
getCategoryTree: () => {
return request.get<any, CategoryTree[]>('/categories')
},
// Get category by id
getCategoryById: (id: string) => {
return request.get<any, Category>(`/categories/${id}`)
},
// Create category
createCategory: (data: CreateCategoryRequest) => {
return request.post<any, Category>('/categories', data)
},
// Update category
updateCategory: (id: string, data: UpdateCategoryRequest) => {
return request.patch<any, Category>(`/categories/${id}`, data)
},
// Delete category
deleteCategory: (id: string) => {
return request.delete(`/categories/${id}`)
},
}
+34 -34
View File
@@ -1,35 +1,35 @@
import request from './request'
import type {
Country,
CreateCountryRequest,
UpdateCountryRequest,
CountryFilter,
PaginatedResult,
} from '@/types'
export const countriesApi = {
// Get countries list
getCountriesList: (params: CountryFilter) => {
return request.get<any, PaginatedResult<Country>>('/countries', { params })
},
// Get country by id
getCountryById: (id: string) => {
return request.get<any, Country>(`/countries/${id}`)
},
// Create country
createCountry: (data: CreateCountryRequest) => {
return request.post<any, Country>('/countries', data)
},
// Update country
updateCountry: (id: string, data: UpdateCountryRequest) => {
return request.patch<any, Country>(`/countries/${id}`, data)
},
// Delete country
deleteCountry: (id: string) => {
return request.delete(`/countries/${id}`)
},
import request from './request'
import type {
Country,
CreateCountryRequest,
UpdateCountryRequest,
CountryFilter,
PaginatedResult,
} from '@/types'
export const countriesApi = {
// Get countries list
getCountriesList: (params: CountryFilter) => {
return request.get<any, PaginatedResult<Country>>('/countries', { params })
},
// Get country by id
getCountryById: (id: string) => {
return request.get<any, Country>(`/countries/${id}`)
},
// Create country
createCountry: (data: CreateCountryRequest) => {
return request.post<any, Country>('/countries', data)
},
// Update country
updateCountry: (id: string, data: UpdateCountryRequest) => {
return request.patch<any, Country>(`/countries/${id}`, data)
},
// Delete country
deleteCountry: (id: string) => {
return request.delete(`/countries/${id}`)
},
}
+39 -39
View File
@@ -1,29 +1,29 @@
import request from './request'
import type {
import request from './request'
import type {
Good,
GoodDetail,
CreateGoodRequest,
UpdateGoodRequest,
BatchCreateGoodsRequest,
BatchPriorityRequest,
CreateGoodRequest,
UpdateGoodRequest,
BatchCreateGoodsRequest,
BatchPriorityRequest,
GoodsFilter,
PaginatedResult,
CreateCustomGoodRequest,
UpdateCustomGoodContentRequest,
} from '@/types'
export const goodsApi = {
// Get goods list
getGoodsList: (params: GoodsFilter) => {
return request.get<any, PaginatedResult<Good>>('/goods', { params })
},
// Get good by id
getGoodById: (id: string) => {
} from '@/types'
export const goodsApi = {
// Get goods list
getGoodsList: (params: GoodsFilter) => {
return request.get<any, PaginatedResult<Good>>('/goods', { params })
},
// Get good by id
getGoodById: (id: string) => {
return request.get<any, GoodDetail>(`/goods/${id}`)
},
// Create good
},
// Create good
createGood: (data: CreateGoodRequest) => {
return request.post<any, Good>('/goods', data)
},
@@ -35,24 +35,24 @@ export const goodsApi = {
updateCustomGoodContent: (id: string, data: UpdateCustomGoodContentRequest) => {
return request.patch<any, GoodDetail>(`/goods/${id}/custom-content`, data)
},
// Update good
updateGood: (id: string, data: UpdateGoodRequest) => {
return request.patch<any, Good>(`/goods/${id}`, data)
},
// Delete good
deleteGood: (id: string) => {
return request.delete(`/goods/${id}`)
},
// Batch create goods
batchCreateGoods: (data: BatchCreateGoodsRequest) => {
return request.post<any, Good[]>('/goods/batch', data)
},
// Batch update priority
batchUpdatePriority: (data: BatchPriorityRequest) => {
return request.patch<any, { count: number }>('/goods/batch-priority', data)
},
// Update good
updateGood: (id: string, data: UpdateGoodRequest) => {
return request.patch<any, Good>(`/goods/${id}`, data)
},
// Delete good
deleteGood: (id: string) => {
return request.delete(`/goods/${id}`)
},
// Batch create goods
batchCreateGoods: (data: BatchCreateGoodsRequest) => {
return request.post<any, Good[]>('/goods/batch', data)
},
// Batch update priority
batchUpdatePriority: (data: BatchPriorityRequest) => {
return request.patch<any, { count: number }>('/goods/batch-priority', data)
},
}
+12 -12
View File
@@ -1,12 +1,12 @@
import request from './request'
import type { OriginGood, OriginGoodsTreeResponse, PaginatedResult } from '@/types'
export const originGoodsApi = {
getTree: () => {
return request.get<any, OriginGoodsTreeResponse>('/origin-goods/tree')
},
getOriginGoodsList: (params: { page?: number; pageSize?: number; keyword?: string }) => {
return request.get<any, PaginatedResult<OriginGood>>('/origin-goods', { params })
},
}
import request from './request'
import type { OriginGood, OriginGoodsTreeResponse, PaginatedResult } from '@/types'
export const originGoodsApi = {
getTree: () => {
return request.get<any, OriginGoodsTreeResponse>('/origin-goods/tree')
},
getOriginGoodsList: (params: { page?: number; pageSize?: number; keyword?: string }) => {
return request.get<any, PaginatedResult<OriginGood>>('/origin-goods', { params })
},
}
+34 -34
View File
@@ -1,35 +1,35 @@
import request from './request'
import type {
Position,
CreatePositionRequest,
UpdatePositionRequest,
PositionFilter,
PaginatedResult,
} from '@/types'
export const positionsApi = {
// Get positions list
getPositionsList: (params: PositionFilter) => {
return request.get<any, PaginatedResult<Position>>('/positions', { params })
},
// Get position by id
getPositionById: (id: string) => {
return request.get<any, Position>(`/positions/${id}`)
},
// Create position
createPosition: (data: CreatePositionRequest) => {
return request.post<any, Position>('/positions', data)
},
// Update position
updatePosition: (id: string, data: UpdatePositionRequest) => {
return request.patch<any, Position>(`/positions/${id}`, data)
},
// Delete position
deletePosition: (id: string) => {
return request.delete(`/positions/${id}`)
},
import request from './request'
import type {
Position,
CreatePositionRequest,
UpdatePositionRequest,
PositionFilter,
PaginatedResult,
} from '@/types'
export const positionsApi = {
// Get positions list
getPositionsList: (params: PositionFilter) => {
return request.get<any, PaginatedResult<Position>>('/positions', { params })
},
// Get position by id
getPositionById: (id: string) => {
return request.get<any, Position>(`/positions/${id}`)
},
// Create position
createPosition: (data: CreatePositionRequest) => {
return request.post<any, Position>('/positions', data)
},
// Update position
updatePosition: (id: string, data: UpdatePositionRequest) => {
return request.patch<any, Position>(`/positions/${id}`, data)
},
// Delete position
deletePosition: (id: string) => {
return request.delete(`/positions/${id}`)
},
}
+81 -81
View File
@@ -1,81 +1,81 @@
import axios from 'axios'
import type { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE || '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
// Session tokens live in HttpOnly cookies — send them along.
withCredentials: true,
})
// Response interceptor
request.interceptors.response.use(
(response: AxiosResponse) => {
// Backend wraps everything in { data, success } — unwrap to data
const body = response.data
if (body && typeof body === 'object' && 'success' in body && 'data' in body) {
return body.data
}
return body
},
async (error: AxiosError) => {
// Access token expired: try the refresh cookie once, then retry the
// original request. A second 401 (refresh failed) logs the user out.
const config = error.config as (InternalAxiosRequestConfig & { _retried?: boolean }) | undefined
if (
error.response?.status === 401 &&
config &&
!config._retried &&
!config.url?.includes('/auth/login') &&
!config.url?.includes('/auth/refresh')
) {
config._retried = true
try {
await axios.post(
`${import.meta.env.VITE_API_BASE || '/api'}/auth/refresh`,
{},
{ withCredentials: true },
)
return request.request(config)
} catch {
// fall through to the 401 handling below
}
}
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
ElMessage.error('Unauthorized, please login')
router.push('/login')
break
case 403:
ElMessage.error('Forbidden')
break
case 404:
ElMessage.error('Resource not found')
break
case 500:
ElMessage.error('Server error')
break
default:
const errorMessage = (data as any)?.message || 'Request failed'
ElMessage.error(errorMessage)
}
} else if (error.request) {
ElMessage.error('Network error')
} else {
ElMessage.error('Request failed')
}
return Promise.reject(error)
}
)
export default request
import axios from 'axios'
import type { AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE || '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
// Session tokens live in HttpOnly cookies — send them along.
withCredentials: true,
})
// Response interceptor
request.interceptors.response.use(
(response: AxiosResponse) => {
// Backend wraps everything in { data, success } — unwrap to data
const body = response.data
if (body && typeof body === 'object' && 'success' in body && 'data' in body) {
return body.data
}
return body
},
async (error: AxiosError) => {
// Access token expired: try the refresh cookie once, then retry the
// original request. A second 401 (refresh failed) logs the user out.
const config = error.config as (InternalAxiosRequestConfig & { _retried?: boolean }) | undefined
if (
error.response?.status === 401 &&
config &&
!config._retried &&
!config.url?.includes('/auth/login') &&
!config.url?.includes('/auth/refresh')
) {
config._retried = true
try {
await axios.post(
`${import.meta.env.VITE_API_BASE || '/api'}/auth/refresh`,
{},
{ withCredentials: true },
)
return request.request(config)
} catch {
// fall through to the 401 handling below
}
}
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
ElMessage.error('Unauthorized, please login')
router.push('/login')
break
case 403:
ElMessage.error('Forbidden')
break
case 404:
ElMessage.error('Resource not found')
break
case 500:
ElMessage.error('Server error')
break
default:
const errorMessage = (data as any)?.message || 'Request failed'
ElMessage.error(errorMessage)
}
} else if (error.request) {
ElMessage.error('Network error')
} else {
ElMessage.error('Request failed')
}
return Promise.reject(error)
}
)
export default request
+16 -16
View File
@@ -1,13 +1,13 @@
import request from './request'
import type { SyncLog } from '@/types'
export const syncApi = {
syncProducts: () => {
return request.post<any, { message: string }>('/sync/products')
},
import request from './request'
import type { SyncLog } from '@/types'
export const syncApi = {
syncProducts: () => {
return request.post<any, { message: string }>('/sync/products')
},
syncCategories: () => {
return request.post<any, { message: string }>('/sync/categories')
return request.post<any, { message: string }>('/sync/categories')
},
syncProductDetails: () => {
@@ -19,10 +19,10 @@ export const syncApi = {
`/sync/products/${goodId}/detail`,
)
},
getSyncStatus: (limit?: number) => {
return request.get<any, SyncLog[]>('/sync/status', {
params: limit ? { limit } : undefined,
})
},
}
getSyncStatus: (limit?: number) => {
return request.get<any, SyncLog[]>('/sync/status', {
params: limit ? { limit } : undefined,
})
},
}
+33 -33
View File
@@ -1,33 +1,33 @@
import request from './request'
import type {
TagGroup,
CreateTagGroupRequest,
UpdateTagGroupRequest,
ReorderTagGroupsRequest,
} from '@/types'
export const tagGroupsApi = {
getTagGroupsList: () => {
return request.get<any, TagGroup[]>('/tag-groups')
},
getTagGroupById: (id: string) => {
return request.get<any, TagGroup>(`/tag-groups/${id}`)
},
createTagGroup: (data: CreateTagGroupRequest) => {
return request.post<any, TagGroup>('/tag-groups', data)
},
updateTagGroup: (id: string, data: UpdateTagGroupRequest) => {
return request.patch<any, TagGroup>(`/tag-groups/${id}`, data)
},
deleteTagGroup: (id: string) => {
return request.delete(`/tag-groups/${id}`)
},
reorderTagGroups: (data: ReorderTagGroupsRequest) => {
return request.patch('/tag-groups/sort', data)
},
}
import request from './request'
import type {
TagGroup,
CreateTagGroupRequest,
UpdateTagGroupRequest,
ReorderTagGroupsRequest,
} from '@/types'
export const tagGroupsApi = {
getTagGroupsList: () => {
return request.get<any, TagGroup[]>('/tag-groups')
},
getTagGroupById: (id: string) => {
return request.get<any, TagGroup>(`/tag-groups/${id}`)
},
createTagGroup: (data: CreateTagGroupRequest) => {
return request.post<any, TagGroup>('/tag-groups', data)
},
updateTagGroup: (id: string, data: UpdateTagGroupRequest) => {
return request.patch<any, TagGroup>(`/tag-groups/${id}`, data)
},
deleteTagGroup: (id: string) => {
return request.delete(`/tag-groups/${id}`)
},
reorderTagGroups: (data: ReorderTagGroupsRequest) => {
return request.patch('/tag-groups/sort', data)
},
}
+40 -40
View File
@@ -1,41 +1,41 @@
import request from './request'
import type {
Tag,
CreateTagRequest,
UpdateTagRequest,
TagFilter,
ReorderTagsRequest,
PaginatedResult,
} from '@/types'
export const tagsApi = {
// Get tags list
getTagsList: (params: TagFilter) => {
return request.get<any, PaginatedResult<Tag>>('/tags', { params })
},
// Get tag by id
getTagById: (id: string) => {
return request.get<any, Tag>(`/tags/${id}`)
},
// Create tag
createTag: (data: CreateTagRequest) => {
return request.post<any, Tag>('/tags', data)
},
// Update tag
updateTag: (id: string, data: UpdateTagRequest) => {
return request.patch<any, Tag>(`/tags/${id}`, data)
},
// Delete tag
deleteTag: (id: string) => {
return request.delete(`/tags/${id}`)
},
// Reorder tags (and/or move across groups)
reorderTags: (data: ReorderTagsRequest) => {
return request.patch('/tags/sort', data)
},
import request from './request'
import type {
Tag,
CreateTagRequest,
UpdateTagRequest,
TagFilter,
ReorderTagsRequest,
PaginatedResult,
} from '@/types'
export const tagsApi = {
// Get tags list
getTagsList: (params: TagFilter) => {
return request.get<any, PaginatedResult<Tag>>('/tags', { params })
},
// Get tag by id
getTagById: (id: string) => {
return request.get<any, Tag>(`/tags/${id}`)
},
// Create tag
createTag: (data: CreateTagRequest) => {
return request.post<any, Tag>('/tags', data)
},
// Update tag
updateTag: (id: string, data: UpdateTagRequest) => {
return request.patch<any, Tag>(`/tags/${id}`, data)
},
// Delete tag
deleteTag: (id: string) => {
return request.delete(`/tags/${id}`)
},
// Reorder tags (and/or move across groups)
reorderTags: (data: ReorderTagsRequest) => {
return request.patch('/tags/sort', data)
},
}
+14 -14
View File
@@ -1,14 +1,14 @@
import request from './request'
export interface UploadResult {
url: string
filename: string
}
export const uploadApi = {
uploadImage: (file: File) => {
const formData = new FormData()
formData.append('file', file)
return request.post<any, UploadResult>('/upload/image', formData)
},
}
import request from './request'
export interface UploadResult {
url: string
filename: string
}
export const uploadApi = {
uploadImage: (file: File) => {
const formData = new FormData()
formData.append('file', file)
return request.post<any, UploadResult>('/upload/image', formData)
},
}