再次优化筛选

This commit is contained in:
yml2213
2026-06-13 18:19:45 +08:00
parent 55326b7d06
commit 9965150521
5 changed files with 301 additions and 7 deletions
+187
View File
@@ -0,0 +1,187 @@
# 快手小店应用服务 MRD/PRD
## 1. 开发者公司介绍
### a) 公司体量、人员数量
本应用由自研技术团队开发和维护,团队具备前后端开发、产品运营、客服处理和系统运维能力。系统已完成商品发布、订单履约、商家交接、买家确认、售后争议、后台管理等核心能力建设。
公司主体、人员数量、联系方式等信息请按实际情况补充。
### b) 公司提供的所有服务应用
目前主要提供虚拟商品/服务类订单履约系统,包含商家商品管理、订单管理、商家发码、买家确认、核销记录、客服介入、证据上传和后台审计等服务。
## 2. 应用服务在其他平台表现
### a) 使用用户数量、活跃用户量、留存率等
当前应用处于自研和业务验证阶段,已完成核心业务流程搭建。
### b) 销售价格报表
- 目前为商家自研自用,无需价格相关
## 3. 应用服务功能简介
### a) 功能介绍
本应用面向快手小店商家,主要解决虚拟服务、数字权益、预约服务等订单的发码和核销问题。
核心功能包括:
- 同步快手小店订单。
- 商家在订单支付后发码或提交交接说明。
- 买家查看发码内容并确认收到。
- 商家根据订单或核销码完成核销。
- 保存发码、确认、核销和售后处理记录。
- 支持客服查看订单、交接记录、证据附件和操作日志。
### b) 功能使用流程
1. 买家在快手小店下单并完成支付。
2. 应用同步订单信息,生成待发码订单。
3. 商家进入后台,对订单填写服务码、交接说明或履约信息。
4. 买家查看发码内容并确认收到。
5. 买家使用服务时出示订单或核销码。
6. 商家核验后完成核销。
7. 如出现售后问题,客服可查看全链路记录并处理。
## 4. 应用服务功能详述
### a) 详细介绍功能点
1. 订单管理
商家可查看快手订单状态,筛选待发码、待核销、已完成、售后中订单。
2. 商家发码
商家可对已支付订单填写服务码、预约码、账号交接说明或其他履约内容,并保存发码记录。
3. 买家确认
买家收到服务信息后,可确认已收到,系统记录确认时间和订单状态。
4. 商家核销
商家输入或扫描核销码,系统校验订单是否已支付、是否已发码、是否重复核销、是否存在售后。校验通过后完成核销。
5. 履约留痕
系统保存发码记录、核销记录、聊天记录、证据附件和客服处理记录,方便后续查询。
6. 售后处理
如订单出现异常,客服可查看订单详情、发码内容、核销状态和证据材料,并进行处理。
### b) 系统主要功能截图
提交前建议补充以下全景截图:
- 商家订单列表页。
- 订单详情页。
- 商家发码页面。
- 商家核销页面。
- 售后/客服处理页面。
- 后台审计日志页面。
### c) 系统子功能截图
提交前建议补充以下全景截图:
- 订单筛选功能。
- 发码内容填写功能。
- 买家确认功能。
- 核销成功/失败提示。
- 证据上传功能。
- 操作日志详情。
## 5. 应用服务实现机制
### a) 服务数据层
系统后端使用 Go + Gin,数据库使用 MySQL,文件存储使用对象存储,前端使用 Vue 3 + TypeScript。
主要数据包括:
- 商家/买家用户信息。
- 快手订单映射信息。
- 发码记录。
- 核销记录。
- 售后和争议记录。
- 文件证据。
- 后台操作审计日志。
### b) 服务业务流层
业务流程为:
快手订单支付成功 -> 应用同步订单 -> 商家发码 -> 买家确认 -> 商家核销 -> 订单完成。
异常流程为:
订单售后中或退款中 -> 暂停核销 -> 客服查看证据 -> 处理售后结果 -> 恢复或关闭订单。
### c) 服务接口调用
系统会调用快手开放平台接口完成订单同步、订单状态回调、履约状态反馈和核销状态反馈。内部接口用于商家发码、买家确认、商家核销、记录查询和后台审核。
## 6. 应用服务对接接口和反馈字段
### a) 具体接口调用和字段反馈
拟对接能力:
- 店铺授权接口:获取商家店铺授权。
- 订单查询接口:同步订单号、商品、SKU、支付状态、售后状态。
- 订单回调接口:接收支付成功、关闭、退款、售后变更。
- 履约反馈接口:商家发码后反馈发码状态。
- 核销反馈接口:商家核销后反馈核销结果。
主要字段:
- `shop_id`:店铺 ID。
- `order_id`:快手订单 ID。
- `item_id`:商品 ID。
- `sku_id`SKU ID。
- `order_status`:订单状态。
- `pay_time`:支付时间。
- `delivery_status`:发码状态。
- `verify_code`:核销码。
- `verify_status`:核销状态。
- `verify_time`:核销时间。
### b) 回调地址、回调原因
正式域名待补充,建议配置:
- `POST https://{正式域名}/api/open/kuaishou/callbacks/order`:订单支付、关闭、状态变化回调。
- `POST https://{正式域名}/api/open/kuaishou/callbacks/after-sale`:退款、售后状态变化回调。
- `POST https://{正式域名}/api/open/kuaishou/callbacks/verify`:核销状态变化回调。
回调处理会进行签名校验、幂等判断、状态更新和日志记录。
## 7. 应用服务服务市场信息
### a) 应用 icon 图
待补充。建议图标突出“发码、核销、订单履约”含义,不使用快手官方标识。
### b) 应用服务介绍
快手小店发码核销助手是一款面向商家的订单履约工具。商家可在订单支付后快速发码,买家确认收到后,商家可根据订单或核销码完成核销。系统会保存发码、确认、核销、售后和客服处理记录,帮助商家提升履约效率,减少漏发、错发、重复核销和售后纠纷。
### c) 价格表
价格待补充,可参考:
| 版本 | 功能 | 价格 |
| --- | --- | --- |
| 基础版 | 订单同步、手动发码、手动核销 | 待补充 |
| 专业版 | 批量发码、核销统计、证据上传 | 待补充 |
| 企业版 | 多店铺、多角色、审计报表、接口日志 | 待补充 |
## 8. 自研发软件著作权
待补充。
如已有软件著作权,请补充证书截图、证书编号、软件名称、著作权人信息,并确保著作权人与店铺或开发者实名认证主体一致。
如暂未取得,可备注:
本系统为自研系统,相关软件著作权材料正在准备中。系统代码、业务流程、前后端页面和数据库设计均由开发团队自主研发。
@@ -1,12 +1,16 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import type { LocationQuery } from 'vue-router' import type { LocationQuery } from 'vue-router'
import { import {
clearStoredHomeQuery,
decodeHomeQueryJson, decodeHomeQueryJson,
encodeHomeQueryJson, encodeHomeQueryJson,
hasHomeQueryValues,
isSameQuery, isSameQuery,
mergeHomeQuery, mergeHomeQuery,
readStoredHomeQuery,
readQueryNumber, readQueryNumber,
readQueryString, readQueryString,
storeHomeQuery,
} from '@/features/listings/composables/useHomeFilterQuery' } from '@/features/listings/composables/useHomeFilterQuery'
describe('useHomeFilterQuery', () => { describe('useHomeFilterQuery', () => {
@@ -51,4 +55,27 @@ describe('useHomeFilterQuery', () => {
server: [], server: [],
}) })
}) })
it('应该判断并缓存首页筛选参数', () => {
const storage = new Map<string, string>()
vi.stubGlobal('window', {
sessionStorage: {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key),
},
})
const storageKey = 'hfb.home.filters.test'
const query = { keyword: '测试', zone: 'gift' }
try {
expect(hasHomeQueryValues(query, ['keyword', 'zone'])).toBe(true)
storeHomeQuery(storageKey, query)
expect(readStoredHomeQuery(storageKey)).toEqual(query)
clearStoredHomeQuery(storageKey)
expect(readStoredHomeQuery(storageKey)).toEqual({})
} finally {
vi.unstubAllGlobals()
}
})
}) })
@@ -29,13 +29,16 @@ export const mobileHomeFilterQueryKeys = ['keyword', 'sort', 'zone', 'filters',
export type HomeQueryValue = string | number | undefined export type HomeQueryValue = string | number | undefined
export function readQueryString(query: LocationQuery, key: string) { export const desktopHomeFilterStorageKey = 'hfb.home.filters.desktop'
export const mobileHomeFilterStorageKey = 'hfb.home.filters.mobile'
export function readQueryString(query: LocationQuery | LocationQueryRaw, key: string) {
const value = query[key] const value = query[key]
if (Array.isArray(value)) return String(value[0] || '') if (Array.isArray(value)) return String(value[0] || '')
return typeof value === 'string' ? value : '' return value === undefined || value === null ? '' : String(value)
} }
export function readQueryNumber(query: LocationQuery, key: string) { export function readQueryNumber(query: LocationQuery | LocationQueryRaw, key: string) {
const value = readQueryString(query, key) const value = readQueryString(query, key)
if (value === '') return undefined if (value === '') return undefined
const number = Number(value) const number = Number(value)
@@ -66,12 +69,62 @@ export function isSameQuery(left: LocationQuery, right: LocationQueryRaw) {
return stableQuerySignature(left) === stableQuerySignature(right) return stableQuerySignature(left) === stableQuerySignature(right)
} }
export function hasHomeQueryValues(
query: LocationQuery | LocationQueryRaw,
keys: readonly string[]
) {
return keys.some(key => {
const value = query[key]
if (Array.isArray(value)) return value.some(item => item !== undefined && item !== '')
return value !== undefined && value !== ''
})
}
export function readStoredHomeQuery(storageKey: string): LocationQueryRaw {
if (typeof window === 'undefined') return {}
try {
const raw = window.sessionStorage.getItem(storageKey)
if (!raw) return {}
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {}
const query: LocationQueryRaw = {}
for (const [key, value] of Object.entries(parsed)) {
if (typeof value === 'string' && value !== '') query[key] = value
}
return query
} catch {
return {}
}
}
export function storeHomeQuery(storageKey: string, query: LocationQueryRaw) {
if (typeof window === 'undefined') return
try {
window.sessionStorage.setItem(storageKey, JSON.stringify(query))
} catch {
// 浏览器禁用 sessionStorage 时忽略,URL 仍然可用。
}
}
export function clearStoredHomeQuery(storageKey: string) {
if (typeof window === 'undefined') return
try {
window.sessionStorage.removeItem(storageKey)
} catch {
// 浏览器禁用 sessionStorage 时忽略。
}
}
export function encodeHomeQueryJson(value: unknown) { export function encodeHomeQueryJson(value: unknown) {
if (!hasMeaningfulValue(value)) return undefined if (!hasMeaningfulValue(value)) return undefined
return JSON.stringify(value) return JSON.stringify(value)
} }
export function decodeHomeQueryJson<T>(query: LocationQuery, key: string, fallback: T): T { export function decodeHomeQueryJson<T>(
query: LocationQuery | LocationQueryRaw,
key: string,
fallback: T
): T {
const raw = readQueryString(query, key) const raw = readQueryString(query, key)
if (!raw) return fallback if (!raw) return fallback
try { try {
@@ -13,11 +13,16 @@ import {
} from '@/features/listings/api/listingOptions' } from '@/features/listings/api/listingOptions'
import { useHomeFilters } from '@/features/listings/composables/useHomeFilters' import { useHomeFilters } from '@/features/listings/composables/useHomeFilters'
import { import {
clearStoredHomeQuery,
desktopHomeFilterStorageKey,
desktopHomeFilterQueryKeys, desktopHomeFilterQueryKeys,
hasHomeQueryValues,
isSameQuery, isSameQuery,
mergeHomeQuery, mergeHomeQuery,
readStoredHomeQuery,
readQueryNumber, readQueryNumber,
readQueryString, readQueryString,
storeHomeQuery,
type HomeQueryValue, type HomeQueryValue,
} from '@/features/listings/composables/useHomeFilterQuery' } from '@/features/listings/composables/useHomeFilterQuery'
import { useListingQuery } from '@/features/listings/composables/useListingQuery' import { useListingQuery } from '@/features/listings/composables/useListingQuery'
@@ -151,6 +156,7 @@ function updateFilters(partial: Partial<typeof filters>) {
} }
function handleResetFilters() { function handleResetFilters() {
clearStoredHomeQuery(desktopHomeFilterStorageKey)
resetFilters() resetFilters()
activeZone.value = 'all' activeZone.value = 'all'
sortBy.value = 'recommended' sortBy.value = 'recommended'
@@ -158,7 +164,10 @@ function handleResetFilters() {
} }
function applyRouteQueryToHomeState() { function applyRouteQueryToHomeState() {
const query = route.query const storedQuery = readStoredHomeQuery(desktopHomeFilterStorageKey)
const query = hasHomeQueryValues(route.query, desktopHomeFilterQueryKeys)
? route.query
: storedQuery
filters.keyword = readQueryString(query, 'keyword') filters.keyword = readQueryString(query, 'keyword')
filters.server = readQueryString(query, 'server') filters.server = readQueryString(query, 'server')
filters.region = readQueryString(query, 'region') filters.region = readQueryString(query, 'region')
@@ -193,6 +202,9 @@ function syncHomeQuery() {
desktopHomeFilterQueryKeys, desktopHomeFilterQueryKeys,
buildDesktopHomeQueryValues() buildDesktopHomeQueryValues()
) )
if (hasHomeQueryValues(query, desktopHomeFilterQueryKeys)) {
storeHomeQuery(desktopHomeFilterStorageKey, query)
}
if (isSameQuery(route.query, query)) return if (isSameQuery(route.query, query)) return
router.replace({ path: '/', query }) router.replace({ path: '/', query })
} }
@@ -224,6 +236,7 @@ function buildDesktopHomeQueryValues(): Record<string, HomeQueryValue> {
} }
} }
syncHomeQuery()
loadHome() loadHome()
</script> </script>
@@ -23,12 +23,17 @@ import {
} from '@/features/listings/api/homeConfig' } from '@/features/listings/api/homeConfig'
import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue' import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue'
import { import {
clearStoredHomeQuery,
decodeHomeQueryJson, decodeHomeQueryJson,
encodeHomeQueryJson, encodeHomeQueryJson,
hasHomeQueryValues,
isSameQuery, isSameQuery,
mergeHomeQuery, mergeHomeQuery,
mobileHomeFilterStorageKey,
mobileHomeFilterQueryKeys, mobileHomeFilterQueryKeys,
readStoredHomeQuery,
readQueryString, readQueryString,
storeHomeQuery,
type HomeQueryValue, type HomeQueryValue,
} from '@/features/listings/composables/useHomeFilterQuery' } from '@/features/listings/composables/useHomeFilterQuery'
import { import {
@@ -384,6 +389,7 @@ function selectZone(zoneKey: string) {
} }
function clearFilters() { function clearFilters() {
clearStoredHomeQuery(mobileHomeFilterStorageKey)
selectedFilters.value = {} selectedFilters.value = {}
rangeFilters.value = {} rangeFilters.value = {}
searchValue.value = '' searchValue.value = ''
@@ -466,7 +472,10 @@ function parseOptionalNumber(value: string) {
} }
function applyRouteQueryToMobileState() { function applyRouteQueryToMobileState() {
const query = route.query const storedQuery = readStoredHomeQuery(mobileHomeFilterStorageKey)
const query = hasHomeQueryValues(route.query, mobileHomeFilterQueryKeys)
? route.query
: storedQuery
searchValue.value = readQueryString(query, 'keyword') searchValue.value = readQueryString(query, 'keyword')
activeSort.value = readQueryString(query, 'sort') || 'recommended' activeSort.value = readQueryString(query, 'sort') || 'recommended'
activeZone.value = readQueryString(query, 'zone') || 'all' activeZone.value = readQueryString(query, 'zone') || 'all'
@@ -480,6 +489,9 @@ function applyRouteQueryToMobileState() {
function syncMobileHomeQuery() { function syncMobileHomeQuery() {
const query = mergeHomeQuery(route.query, mobileHomeFilterQueryKeys, buildMobileHomeQueryValues()) const query = mergeHomeQuery(route.query, mobileHomeFilterQueryKeys, buildMobileHomeQueryValues())
if (hasHomeQueryValues(query, mobileHomeFilterQueryKeys)) {
storeHomeQuery(mobileHomeFilterStorageKey, query)
}
if (isSameQuery(route.query, query)) return if (isSameQuery(route.query, query)) return
router.replace({ path: route.path, query }) router.replace({ path: route.path, query })
} }
@@ -572,6 +584,8 @@ function chipTone(label: string) {
if (label.includes('方便')) return 'time' if (label.includes('方便')) return 'time'
return 'default' return 'default'
} }
syncMobileHomeQuery()
</script> </script>
<template> <template>