diff --git a/supplied-h5/src/api/supplied-service.js b/supplied-h5/src/api/supplied-service.js
index c6d911d..c228856 100644
--- a/supplied-h5/src/api/supplied-service.js
+++ b/supplied-h5/src/api/supplied-service.js
@@ -38,3 +38,19 @@ export const postFunction = async (name, body = {}) => {
return payload.data
}
+
+export const uploadImageFunction = async (file, folder = 'after-sale') => {
+ const contentBase64 = await new Promise((resolve, reject) => {
+ const reader = new FileReader()
+ reader.onload = () => resolve(String(reader.result || ''))
+ reader.onerror = reject
+ reader.readAsDataURL(file)
+ })
+
+ return postFunction('upload', {
+ fileName: file.name,
+ contentType: file.type,
+ contentBase64,
+ folder
+ })
+}
diff --git a/supplied-h5/src/router/index.js b/supplied-h5/src/router/index.js
index 17ffb26..837d982 100644
--- a/supplied-h5/src/router/index.js
+++ b/supplied-h5/src/router/index.js
@@ -14,7 +14,12 @@ import JoyfulSelectionAddressList from '@/view/joyful-selection/address/index.vu
import JoyfulSelectionAddressEdit from '@/view/joyful-selection/address/edit/index.vue'
import JoyfulSelectionOrders from '@/view/joyful-selection/profile/orders/index.vue'
import JoyfulSelectionOrderDetail from '@/view/joyful-selection/profile/order-detail/index.vue'
-import JoyfulSelectionAfterSale from '@/view/joyful-selection/profile/after-sale/index.vue'
+import JoyfulSelectionAfterSale from '@/view/joyful-selection/after-sale/index/index.vue'
+import JoyfulSelectionAfterSaleDetail from '@/view/joyful-selection/after-sale/detail/index.vue'
+import JoyfulSelectionAfterSaleSelect from '@/view/joyful-selection/after-sale/select/index.vue'
+import JoyfulSelectionAfterSaleReturn from '@/view/joyful-selection/after-sale/return/index.vue'
+import JoyfulSelectionAfterSaleExchange from '@/view/joyful-selection/after-sale/exchange/index.vue'
+import JoyfulSelectionMerchant from '@/view/joyful-selection/merchant/index.vue'
import JoyfulSelectionProfile from '@/view/joyful-selection/profile/index.vue'
import JoyfulSelectionPoints from '@/view/joyful-selection/profile/points/index.vue'
import HomeLiving from '@/view/home-living/index.vue'
@@ -45,6 +50,11 @@ export default new Router({
{ path: 'orders', component: JoyfulSelectionOrders },
{ path: 'order-detail', component: JoyfulSelectionOrderDetail },
{ path: 'after-sale', component: JoyfulSelectionAfterSale },
+ { path: 'after-sale/detail', component: JoyfulSelectionAfterSaleDetail },
+ { path: 'after-sale/select', component: JoyfulSelectionAfterSaleSelect },
+ { path: 'after-sale/return', component: JoyfulSelectionAfterSaleReturn },
+ { path: 'after-sale/exchange', component: JoyfulSelectionAfterSaleExchange },
+ { path: 'merchant', component: JoyfulSelectionMerchant },
{ path: 'profile', component: JoyfulSelectionProfile },
{ path: 'points', component: JoyfulSelectionPoints }
]
diff --git a/supplied-h5/src/view/joyful-selection/address/edit/index.vue b/supplied-h5/src/view/joyful-selection/address/edit/index.vue
index 4137f86..a2225ca 100644
--- a/supplied-h5/src/view/joyful-selection/address/edit/index.vue
+++ b/supplied-h5/src/view/joyful-selection/address/edit/index.vue
@@ -90,7 +90,8 @@ export default {
...nextAddress,
addressId: nextAddress.id,
supplierId: this.supplierId,
- userId: this.userId
+ userId: this.userId,
+ isDefault: ['checkout', 'after-sale'].includes(this.$route.query.from)
})
saveChosenAddressId(nextAddress.id)
Toast('保存成功')
diff --git a/supplied-h5/src/view/joyful-selection/address/index.vue b/supplied-h5/src/view/joyful-selection/address/index.vue
index 195f04d..2799b02 100644
--- a/supplied-h5/src/view/joyful-selection/address/index.vue
+++ b/supplied-h5/src/view/joyful-selection/address/index.vue
@@ -81,7 +81,7 @@ export default {
this.chosenAddressId = item.id
saveChosenAddressId(item.id)
- if (this.$route.query.from === 'checkout') {
+ if (['checkout', 'after-sale'].includes(this.$route.query.from)) {
const address = this.addresses.find(addressItem => addressItem.id === item.id)
if (address) {
try {
@@ -101,10 +101,10 @@ export default {
}
},
onAdd() {
- this.$router.push('/joyful-selection/address/edit')
+ this.$router.push({ path: '/joyful-selection/address/edit', query: { from: this.$route.query.from } })
},
onEdit(item) {
- this.$router.push({ path: '/joyful-selection/address/edit', query: { id: item.id } })
+ this.$router.push({ path: '/joyful-selection/address/edit', query: { id: item.id, from: this.$route.query.from } })
}
}
}
diff --git a/supplied-h5/src/view/joyful-selection/after-sale/detail/index.vue b/supplied-h5/src/view/joyful-selection/after-sale/detail/index.vue
new file mode 100644
index 0000000..bbf8d85
--- /dev/null
+++ b/supplied-h5/src/view/joyful-selection/after-sale/detail/index.vue
@@ -0,0 +1,766 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ statusTitle }}
+
{{ statusDesc }}
+
+
+
+
+
+ 售后进度
+
+
+
+
+
+
+ {{ step.title }}
+ {{ step.time || step.subTitle }}
+
+
{{ step.desc }}
+
+
+
+
+
+ 售后商品
+
+
+
+
{{ item.name || '-' }}
+
{{ skuText(item) }}
+
¥{{ item.price || afterSale.amount || '0.00' }}
+
+ x{{ item.number || 1 }}
+
+
+
+
+ {{ returnInfoTitle }}
+ 联系人{{ afterSale.pickupName || '-' }}
+ 联系电话{{ maskedMobile(afterSale.pickupMobile) }}
+ 地址{{ afterSale.pickupAddress || '-' }}
+
+ 快递单号{{ afterSale.userLogisticsCompany || '快递' }} {{ afterSale.userLogisticsNo }}
+
+
+ 快递单号请填写快递单号
+
+ 请在7天内填写快递单号,逾期将自动关闭售后申请
+
+
+
+ 售后信息
+ 售后单号{{ afterSale.serviceOrderId || '-' }}
+ 售后类型{{ afterSale.serviceTypeText || '-' }}
+ 售后原因{{ afterSale.reason || '-' }}
+ 问题描述{{ afterSale.problemDescription || '-' }}
+ 申请时间{{ afterSale.applyTime || '-' }}
+ 取件方式{{ pickupMethodText }}
+ 取件地址{{ pickupAddressText }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/supplied-h5/src/view/joyful-selection/after-sale/exchange/index.vue b/supplied-h5/src/view/joyful-selection/after-sale/exchange/index.vue
new file mode 100644
index 0000000..5636c67
--- /dev/null
+++ b/supplied-h5/src/view/joyful-selection/after-sale/exchange/index.vue
@@ -0,0 +1,819 @@
+
+
+
+
+
+
+
+ 售后商品
+
+
+
+
{{ item.name || '-' }}
+
{{ skuText(item) }}
+
¥{{ item.price || currentSubOrder.amount || '0.00' }}
+
+ x{{ item.number || 1 }}
+
+
+
+
+
+
换货商品
+
+
+
+
+
+
{{ selectedExchangeGoods.name || '-' }}
+
{{ skuText(selectedExchangeGoods) }}
+
¥{{ selectedExchangeGoods.price || '0.00' }}
+
+ x1
+
+ 请选择与原商品价格一致的换货商品
+
+
+
+
+ 提交申请
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/supplied-h5/src/view/joyful-selection/profile/after-sale/index.vue b/supplied-h5/src/view/joyful-selection/after-sale/index/index.vue
similarity index 69%
rename from supplied-h5/src/view/joyful-selection/profile/after-sale/index.vue
rename to supplied-h5/src/view/joyful-selection/after-sale/index/index.vue
index fdcd89a..4995ffb 100644
--- a/supplied-h5/src/view/joyful-selection/profile/after-sale/index.vue
+++ b/supplied-h5/src/view/joyful-selection/after-sale/index/index.vue
@@ -3,10 +3,20 @@
-
+
+
+
+
-
-
+
+
+
@@ -46,15 +57,38 @@ import { Toast } from 'vant'
import { postFunction } from '@/api/supplied-service'
import { useUserStore } from '@/store/user'
+const SKU_COLOR_MAP = {
+ cream: '奶油白',
+ blue: '雾霾蓝',
+ orange: '暖橙色'
+}
+
+const SKU_SIZE_MAP = {
+ standard: '标准款',
+ plus: '升级款'
+}
+
export default {
name: 'JoyfulSelectionAfterSale',
data: () => ({
+ activeTab: 'processing',
afterSaleItems: [],
loading: false,
refreshing: false
}),
computed: {
- ...mapState(useUserStore, ['supplierId', 'userId'])
+ ...mapState(useUserStore, ['supplierId', 'userId']),
+ currentAfterSaleItems() {
+ return this.afterSaleItems.filter(item => {
+ const serviceState = Number(item.serviceState)
+ const isHistory = [2, 5, 6].includes(serviceState)
+
+ return this.activeTab === 'history' ? isHistory : !isHistory
+ })
+ },
+ emptyDescription() {
+ return this.activeTab === 'history' ? '暂无历史售后' : '暂无处理中售后'
+ }
},
created() {
this.loadAfterSales()
@@ -64,6 +98,22 @@ export default {
previewProducts(products = []) {
return products.slice(0, 3)
},
+ skuText(product = {}) {
+ const sku = String(product.sku || product.skuText || '').trim()
+ const parts = sku.split('-').slice(-2).filter(Boolean)
+ const color = SKU_COLOR_MAP[parts[0]] || parts[0]
+ const size = SKU_SIZE_MAP[parts[1]] || parts[1]
+
+ return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
+ },
+ goDetail(item) {
+ this.$router.push({
+ path: '/joyful-selection/after-sale/detail',
+ query: {
+ serviceOrderId: item.serviceOrderId
+ }
+ })
+ },
async loadAfterSales(showLoading = true) {
if (showLoading) {
this.loading = true
@@ -122,7 +172,11 @@ export default {
.after-sale-list {
min-height: calc(100vh - 46px);
box-sizing: border-box;
- padding: 12px 10px 18px;
+ padding-bottom: 18px;
+}
+
+.after-sale-tab-panel {
+ padding: 12px 10px 0;
}
.after-sale-card {
@@ -132,6 +186,16 @@ export default {
background: #fff;
}
+.skeleton-card {
+ padding: 14px 12px;
+ border-radius: 8px;
+ background: #fff;
+}
+
+.skeleton-card ::v-deep .van-skeleton + .van-skeleton {
+ margin-top: 18px;
+}
+
.after-sale-card__header {
display: flex;
align-items: center;
diff --git a/supplied-h5/src/view/joyful-selection/after-sale/return/index.vue b/supplied-h5/src/view/joyful-selection/after-sale/return/index.vue
new file mode 100644
index 0000000..7e6544e
--- /dev/null
+++ b/supplied-h5/src/view/joyful-selection/after-sale/return/index.vue
@@ -0,0 +1,642 @@
+
+
+
+
+
+
+
+ 售后商品
+
+
+
+
{{ item.name || '-' }}
+
{{ skuText(item) }}
+
¥{{ item.price || currentSubOrder.amount || '0.00' }}
+
+ x{{ item.number || 1 }}
+
+
+
+
+
+ 提交申请
+
+
+
+
+
+
+
+
+
diff --git a/supplied-h5/src/view/joyful-selection/after-sale/select/index.vue b/supplied-h5/src/view/joyful-selection/after-sale/select/index.vue
new file mode 100644
index 0000000..1b40767
--- /dev/null
+++ b/supplied-h5/src/view/joyful-selection/after-sale/select/index.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+
+ 售后商品
+
+
+
+
{{ item.name || '-' }}
+
{{ skuText(item) }}
+
¥{{ item.price || currentSubOrder.amount || '0.00' }}
+
+ x{{ item.number || 1 }}
+
+
+
+
+
+ 售后申请提交后,商家会尽快为您处理
+
+
+
+
+
+
diff --git a/supplied-h5/src/view/joyful-selection/merchant/index.vue b/supplied-h5/src/view/joyful-selection/merchant/index.vue
new file mode 100644
index 0000000..e5b9d89
--- /dev/null
+++ b/supplied-h5/src/view/joyful-selection/merchant/index.vue
@@ -0,0 +1,513 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.orderNumberId || item.orderId }}
+ 待发货
+
+
+
+
+
{{ product.name || '-' }}
+
{{ product.sku || '-' }}
+
¥{{ product.price }}
+
+
x{{ product.number || 1 }}
+
+
+ 用户{{ item.userId || '-' }}
+
+
+ 下单时间{{ item.createTime || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.serviceOrderId }}
+ {{ item.serviceTypeText }}
+
+
+
+
+
{{ product.name || '-' }}
+
{{ product.sku || '-' }}
+
¥{{ product.price }}
+
+
x{{ product.number || 1 }}
+
+
+ 原因{{ item.reason || '-' }}
+
+
+ 描述{{ item.problemDescription || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.serviceOrderId }}
+ {{ item.serviceStateText }}
+
+
+
+
+
{{ product.name || '-' }}
+
{{ product.sku || '-' }}
+
¥{{ product.price }}
+
+
x{{ product.number || 1 }}
+
+
+ 寄回单号{{ item.userLogisticsCompany || '' }} {{ item.userLogisticsNo || '-' }}
+
+
+ 取件地址{{ item.pickupAddress || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/supplied-h5/src/view/joyful-selection/order/payment-confirm/index.vue b/supplied-h5/src/view/joyful-selection/order/payment-confirm/index.vue
deleted file mode 100644
index 87248a4..0000000
--- a/supplied-h5/src/view/joyful-selection/order/payment-confirm/index.vue
+++ /dev/null
@@ -1,201 +0,0 @@
-
-
-
-
-
- 支付金额
- ¥ {{ amountText }}
- {{ payTypeText }}
-
-
-
-
-
-
-
-
-
diff --git a/supplied-h5/src/view/joyful-selection/product-data.js b/supplied-h5/src/view/joyful-selection/product-data.js
deleted file mode 100644
index 64e08ff..0000000
--- a/supplied-h5/src/view/joyful-selection/product-data.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import bluetoothEarbudsImage from '@/assets/joyful-selection/products/bluetooth-earbuds.png'
-import miniOvenImage from '@/assets/joyful-selection/products/mini-oven.png'
-import smartSpeakerImage from '@/assets/joyful-selection/products/smart-speaker.png'
-import giftBoxImage from '@/assets/joyful-selection/products/gift-box.png'
-
-const productImages = {
- bluetoothEarbuds: bluetoothEarbudsImage,
- miniOven: miniOvenImage,
- smartSpeaker: smartSpeakerImage,
- giftBox: giftBoxImage
-}
-
-const createProduct = ({ id, name, desc, detail, price, sales, image, categoryId, category, tag = '赠品' }) => ({
- id,
- name,
- desc,
- detail: detail || desc,
- price,
- originPrice: (Number(price) * 1.25).toFixed(2),
- sales,
- image,
- categoryId,
- category,
- tag
-})
-
-export const joyfulCategories = [
- { id: 'recommend', name: '推荐' },
- { id: 'homeLiving', name: '居家生活' },
- { id: 'digitalAppliance', name: '数码家电' },
- { id: 'beautyCare', name: '美妆个护' },
- { id: 'foodDrink', name: '食品饮料' },
- { id: 'motherBaby', name: '母婴玩具' },
- { id: 'fashionBag', name: '服饰鞋包' },
- { id: 'sportsOutdoor', name: '运动户外' }
-]
-
-const categoryNameMap = joyfulCategories.reduce((map, category) => {
- map[category.id] = category.name
- return map
-}, {})
-
-const rawProducts = [
- ['cream-oven', '奶油白家用迷你烤箱', '小巧不占地,早餐烘焙好帮手', '129.00', '1.2万件', productImages.miniOven, 'homeLiving'],
- ['cotton-bedding', '100%纯棉舒适四件套', '柔软亲肤,安心睡眠', '179.00', '6453件', productImages.miniOven, 'homeLiving'],
- ['aroma-gift', '橙意香薰礼盒套装', '淡雅留香,装点居家空间', '59.90', '8321件', productImages.giftBox, 'homeLiving'],
- ['desktop-vase', '简约北欧陶瓷花瓶', '清新配色,点亮桌面角落', '39.90', '3180件', productImages.giftBox, 'homeLiving'],
- ['earbuds-air2', '小米蓝牙耳机 Air2 SE', '轻盈半入耳,通勤运动都好听', '69.90', '1.2万件', productImages.bluetoothEarbuds, 'digitalAppliance'],
- ['smart-speaker-pro', '黑曜智能音箱 Pro', '语音唤醒,沉浸听感', '99.00', '2.1万件', productImages.smartSpeaker, 'digitalAppliance'],
- ['electric-toothbrush', '智能声波电动牙刷', '高频洁净,呵护口腔', '129.00', '2.1万件', productImages.bluetoothEarbuds, 'digitalAppliance'],
- ['dual-charger', '双口快充充电器 30W', '小巧便携,多设备快充', '69.00', '5632件', productImages.smartSpeaker, 'digitalAppliance'],
- ['skin-care-box', '植萃保湿护肤礼盒', '水润修护,清爽不粘腻', '99.90', '4820件', productImages.giftBox, 'beautyCare'],
- ['aroma-wash', '香氛洗护旅行套装', '温和清洁,清新留香', '59.90', '7932件', productImages.giftBox, 'beautyCare'],
- ['makeup-mirror', '便携补光化妆镜', '高清镜面,随时补妆', '49.90', '3651件', productImages.smartSpeaker, 'beautyCare'],
- ['hand-cream', '滋润护手霜三支装', '细腻滋养,轻柔香气', '39.90', '5280件', productImages.giftBox, 'beautyCare'],
- ['orange-gift', '橙意惊喜零食礼盒', '精致包装,送礼更有心意', '59.90', '6324件', productImages.giftBox, 'foodDrink'],
- ['coffee-cup', '咖啡随行杯组合', '锁温防漏,醇香随行', '49.90', '3520件', productImages.bluetoothEarbuds, 'foodDrink'],
- ['tea-snack', '下午茶点心礼盒', '多种风味,一盒满足', '69.90', '4891件', productImages.giftBox, 'foodDrink'],
- ['fruit-drink', '轻享果饮礼盒', '清爽酸甜,聚会常备', '79.90', '2976件', productImages.giftBox, 'foodDrink'],
- ['baby-bottle', '安心防摔儿童水杯', '轻巧易握,出门好携带', '39.90', '4250件', productImages.bluetoothEarbuds, 'motherBaby'],
- ['toy-blocks', '益智拼搭积木套装', '色彩柔和,陪伴成长', '89.00', '2186件', productImages.giftBox, 'motherBaby'],
- ['kids-night-light', '柔光儿童陪伴夜灯', '温和光线,安心入睡', '69.90', '3548件', productImages.smartSpeaker, 'motherBaby'],
- ['baby-towel', '纯棉柔软浴巾两件装', '亲肤吸水,日常必备', '59.90', '2761件', productImages.miniOven, 'motherBaby'],
- ['laptop-backpack', '大容量双肩电脑背包', '分区收纳,通勤出行轻松装', '149.00', '1.6万件', productImages.giftBox, 'fashionBag'],
- ['mini-bag', '轻巧通勤斜挎小包', '简洁百搭,日常实用', '79.90', '5321件', productImages.giftBox, 'fashionBag'],
- ['silk-scarf', '柔软撞色丝巾礼盒', '细腻亲肤,点亮穿搭', '69.90', '2680件', productImages.giftBox, 'fashionBag'],
- ['canvas-tote', '简约帆布托特包', '大容量开口,随手能装', '49.90', '6412件', productImages.giftBox, 'fashionBag'],
- ['sport-bottle', '运动便携保温水杯', '长效锁温,户外随行', '49.90', '7342件', productImages.bluetoothEarbuds, 'sportsOutdoor'],
- ['neck-massager', '筋膜放松按摩仪 Mini', '轻巧便携,舒缓酸胀', '99.00', '3921件', productImages.smartSpeaker, 'sportsOutdoor'],
- ['camping-light', '户外便携露营灯', '多档照明,夜晚更安心', '79.00', '2568件', productImages.smartSpeaker, 'sportsOutdoor'],
- ['fitness-band', '弹力健身拉力带套装', '居家训练,轻松塑形', '29.90', '4396件', productImages.miniOven, 'sportsOutdoor']
-]
-
-export const joyfulProductList = rawProducts.map(([id, name, desc, price, sales, image, categoryId]) =>
- createProduct({
- id,
- name,
- desc,
- price,
- sales,
- image,
- categoryId,
- category: categoryNameMap[categoryId]
- })
-)
-
-export const joyfulProducts = [
- {
- id: 'bluetooth-earbuds',
- name: '小米蓝牙耳机 Air2 SE',
- desc: '轻盈半入耳,通勤运动都好听',
- detail: '舒适半入耳设计,配合便携充电盒,日常通勤、运动和在线会议都能保持清晰稳定的聆听体验。',
- price: '69.90',
- originPrice: '99.90',
- category: '数码家电',
- tag: '积分兑换',
- image: bluetoothEarbudsImage
- },
- {
- id: 'mini-oven',
- name: '奶油白家用迷你烤箱',
- desc: '小巧不占地,早餐烘焙好帮手',
- detail: '紧凑台式机身搭配可视化炉门,适合烤吐司、蛋挞和小份点心,让厨房日常更轻松。',
- price: '129.00',
- originPrice: '169.00',
- category: '居家生活',
- tag: '新品上架',
- image: miniOvenImage
- },
- {
- id: 'smart-speaker',
- name: '黑曜智能音箱 Pro',
- desc: '语音唤醒,沉浸听感',
- detail: '圆柱一体式声学结构,支持语音互动和日常音乐播放,适合卧室、书房与办公桌面使用。',
- price: '99.00',
- originPrice: '139.00',
- category: '数码家电',
- tag: '热门推荐',
- image: smartSpeakerImage
- },
- {
- id: 'gift-box',
- name: '橙意惊喜礼盒',
- desc: '精致包装,送礼更有心意',
- detail: '亮橙礼盒搭配丝带包装,适合节日福利、积分兑换礼和日常心意礼赠场景。',
- price: '59.90',
- originPrice: '89.90',
- category: '食品饮料',
- tag: '品质精选',
- image: giftBoxImage
- },
- ...joyfulProductList
-]
-
-export const getJoyfulProduct = id => joyfulProducts.find(item => item.id === id) || joyfulProducts[0]
diff --git a/supplied-h5/src/view/joyful-selection/profile/order-detail/index.vue b/supplied-h5/src/view/joyful-selection/profile/order-detail/index.vue
index 7bfd057..a42e92d 100644
--- a/supplied-h5/src/view/joyful-selection/profile/order-detail/index.vue
+++ b/supplied-h5/src/view/joyful-selection/profile/order-detail/index.vue
@@ -74,7 +74,7 @@
订单编号{{ currentSubOrder.orderNumberId || currentSubOrder.orderId || '-' }}
下单时间{{ currentSubOrder.createTime || currentOrder.createTime || '-' }}
支付方式{{ payMethodText }}
- 支付时间{{ currentOrder.payTime || '-' }}
+ 支付时间{{ payTimeText }}
商品金额¥{{ currentSubOrder.amount }}
运费¥{{ freightText }}
实付款¥{{ paidAmountText }}
@@ -177,6 +177,9 @@ export default {
freightText() {
return Number(this.currentSubOrder?.freightCost || 0).toFixed(2)
},
+ payTimeText() {
+ return this.currentSubOrder?.payTime || this.currentOrder?.payTime || '-'
+ },
paidAmountText() {
return Number(this.currentSubOrder?.amount || 0).toFixed(2)
},
@@ -282,7 +285,7 @@ export default {
}
if (action.key === 'after-sale') {
- this.submitAfterSale(action)
+ this.goAfterSaleSelect()
return
}
@@ -303,6 +306,15 @@ export default {
})
this.$router.push('/joyful-selection/payment')
},
+ goAfterSaleSelect() {
+ this.$router.push({
+ path: '/joyful-selection/after-sale/select',
+ query: {
+ mainOrderId: this.currentOrder?.mainOrderId || this.currentOrder?.orderId,
+ orderId: this.currentSubOrder?.orderId
+ }
+ })
+ },
async cancelMainOrder() {
try {
await Dialog.confirm({
diff --git a/supplied-h5/src/view/joyful-selection/profile/orders/index.vue b/supplied-h5/src/view/joyful-selection/profile/orders/index.vue
index 3c75468..1ce9f93 100644
--- a/supplied-h5/src/view/joyful-selection/profile/orders/index.vue
+++ b/supplied-h5/src/view/joyful-selection/profile/orders/index.vue
@@ -343,7 +343,7 @@ export default {
}
if ([ORDER_EVENT_TYPE.RETURN_ORDER, ORDER_EVENT_TYPE.EXCHANGE_ORDER].includes(action.orderType)) {
- this.submitAfterSale(order, action)
+ this.goAfterSaleSelect(order)
return
}
@@ -366,6 +366,15 @@ export default {
})
this.$router.push('/joyful-selection/payment')
},
+ goAfterSaleSelect(order) {
+ this.$router.push({
+ path: '/joyful-selection/after-sale/select',
+ query: {
+ mainOrderId: order.mainOrderId,
+ orderId: order.orderId
+ }
+ })
+ },
async cancelMainOrder(order) {
try {
await Dialog.confirm({
diff --git a/supplied-service/.env.example b/supplied-service/.env.example
index 9658623..f782ee2 100644
--- a/supplied-service/.env.example
+++ b/supplied-service/.env.example
@@ -8,8 +8,6 @@ UPLOAD_TOKEN="replace-me"
# Main points mall bridge API, for example http://localhost/open.
POINTS_MALL_OPEN_API_BASE="http://localhost/open"
-
-# Supplier open API crypto keys used by /supplierOpenApi/pointOrder/queryOrderInfo.
SUPPLIER_OPEN_SM4_KEY_HEX="00112233445566778899aabbccddeeff"
SUPPLIER_OPEN_SM4_IV_HEX="0102030405060708090a0b0c0d0e0f10"
@@ -18,3 +16,9 @@ REDIS_HOST="127.0.0.1"
REDIS_PORT="6379"
REDIS_PASSWORD=""
REDIS_DB="9"
+
+MINIO_ACCESS_KEY="replace-me"
+MINIO_SECRET_KEY="replace-me"
+MINIO_ENDPOINT="http://localhost:9000"
+MINIO_BUCKET="njupt"
+MINIO_REGION="us-east-1"
\ No newline at end of file
diff --git a/supplied-service/prisma/schema.prisma b/supplied-service/prisma/schema.prisma
index cf39589..b9bd4cd 100644
--- a/supplied-service/prisma/schema.prisma
+++ b/supplied-service/prisma/schema.prisma
@@ -92,6 +92,7 @@ model SupplierOrderMain {
remark String? @db.Text
createTime DateTime?
updateTime DateTime?
+ payTime DateTime?
deliveryTime DateTime?
orderFinishTime DateTime?
completeTime DateTime?
@@ -131,6 +132,7 @@ model SupplierOrderSub {
remark String? @db.Text
createTime DateTime?
updateTime DateTime?
+ payTime DateTime?
deliveryTime DateTime?
orderFinishTime DateTime?
completeTime DateTime?
@@ -309,6 +311,13 @@ model SupplierAfterSale {
logisticsState Int @default(0)
amount Decimal @default(0) @db.Decimal(10, 2)
reason String? @db.Text
+ reasonType String?
+ problemDescription String? @db.Text
+ pickupMethod String?
+ pickupName String?
+ pickupMobile String?
+ pickupAddress String? @db.Text
+ images Json?
auditRemark String? @db.Text
userLogisticsNo String?
userLogisticsCompany String?
diff --git a/supplied-service/prisma/sql/order-pay-time.sql b/supplied-service/prisma/sql/order-pay-time.sql
new file mode 100644
index 0000000..6270b2f
--- /dev/null
+++ b/supplied-service/prisma/sql/order-pay-time.sql
@@ -0,0 +1,2 @@
+alter table supplier_order_main add column payTime datetime null;
+alter table supplier_order_sub add column payTime datetime null;
diff --git a/supplied-service/prisma/sql/supplier-after-sale-apply-fields.sql b/supplied-service/prisma/sql/supplier-after-sale-apply-fields.sql
new file mode 100644
index 0000000..a9c99be
--- /dev/null
+++ b/supplied-service/prisma/sql/supplier-after-sale-apply-fields.sql
@@ -0,0 +1,7 @@
+alter table supplier_after_sale add column reasonType varchar(191) null;
+alter table supplier_after_sale add column problemDescription text null;
+alter table supplier_after_sale add column pickupMethod varchar(191) null;
+alter table supplier_after_sale add column pickupName varchar(191) null;
+alter table supplier_after_sale add column pickupMobile varchar(191) null;
+alter table supplier_after_sale add column pickupAddress text null;
+alter table supplier_after_sale add column images json null;
diff --git a/supplied-service/src/controllers/basic.controller.ts b/supplied-service/src/controllers/basic.controller.ts
index 50d3e25..4407ee9 100644
--- a/supplied-service/src/controllers/basic.controller.ts
+++ b/supplied-service/src/controllers/basic.controller.ts
@@ -2,6 +2,8 @@ import type { Context } from '../utils/handler.js'
import { guardMethod } from '../utils/method.js'
import { withErrorBoundary } from '../utils/handler.js'
import { basicService } from '../services/basic.service.js'
+import { minioUploadService, type PreviewInput, type UploadInput } from '../services/minio-upload.service.js'
+import { readJson } from '../utils/request.js'
import { toHttpResponse } from './controller-response.js'
export const loginController = withErrorBoundary(async (request: Request, _context: Context) => {
@@ -22,7 +24,14 @@ export const uploadController = withErrorBoundary(async (request: Request, _cont
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
- return toHttpResponse(basicService.upload())
+ return toHttpResponse(await minioUploadService.upload(await readJson(request)))
+})
+
+export const uploadPreviewController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ return toHttpResponse(minioUploadService.preview(await readJson(request)))
})
export const userInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
diff --git a/supplied-service/src/controllers/merchant.controller.ts b/supplied-service/src/controllers/merchant.controller.ts
new file mode 100644
index 0000000..e1f898c
--- /dev/null
+++ b/supplied-service/src/controllers/merchant.controller.ts
@@ -0,0 +1,69 @@
+import type { Context } from '../utils/handler.js'
+import { guardMethod } from '../utils/method.js'
+import { readJson } from '../utils/request.js'
+import { withErrorBoundary } from '../utils/handler.js'
+import {
+ merchantService,
+ type MerchantAfterSaleListInput,
+ type MerchantOrderListInput,
+ type MerchantShipInput
+} from '../services/merchant.service.js'
+import type { AfterSaleAuditInput, AfterSaleLogisticsInput } from '../services/after-sale.service.js'
+import { toHttpResponse } from './controller-response.js'
+
+type MerchantAfterSaleActionInput = AfterSaleLogisticsInput & {
+ logisticsNo?: string
+ logisticsCompany?: string
+}
+
+export const merchantOrderListController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ return toHttpResponse(await merchantService.listOrders(await readJson(request)))
+})
+
+export const merchantOrderShipController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ return toHttpResponse(await merchantService.shipOrder(await readJson(request)))
+})
+
+export const merchantAfterSaleListController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ return toHttpResponse(await merchantService.listAfterSales(await readJson(request)))
+})
+
+export const merchantAfterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ return toHttpResponse(await merchantService.auditAfterSale(await readJson(request)))
+})
+
+export const merchantAfterSaleReceiveController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ const body = await readJson(request)
+ return toHttpResponse(await merchantService.receiveAfterSale(
+ String(body.serviceOrderId || '').trim(),
+ String(body.supplierId || '').trim()
+ ))
+})
+
+export const merchantAfterSaleShipController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ const body = await readJson(request)
+ return toHttpResponse(await merchantService.shipExchange(
+ String(body.serviceOrderId || '').trim(),
+ String(body.supplierId || '').trim(),
+ String(body.logisticsNo || body.merchantLogisticsNo || '').trim(),
+ String(body.logisticsCompany || body.merchantLogisticsCompany || '').trim()
+ ))
+})
diff --git a/supplied-service/src/controllers/order.controller.ts b/supplied-service/src/controllers/order.controller.ts
index a2522a6..c4d660e 100644
--- a/supplied-service/src/controllers/order.controller.ts
+++ b/supplied-service/src/controllers/order.controller.ts
@@ -6,6 +6,7 @@ import {
afterSaleService,
type AfterSaleApplyInput,
type AfterSaleAuditInput,
+ type AfterSaleDetailInput,
type AfterSaleListInput,
type AfterSaleLogisticsInput
} from '../services/after-sale.service.js'
@@ -99,6 +100,13 @@ export const afterSaleListController = withErrorBoundary(async (request: Request
return toHttpResponse(await afterSaleService.list(await readJson(request)))
})
+export const afterSaleDetailController = withErrorBoundary(async (request: Request, _context: Context) => {
+ const methodResponse = guardMethod(request, ['POST'])
+ if (methodResponse) return methodResponse
+
+ return toHttpResponse(await afterSaleService.detail(await readJson(request)))
+})
+
export const afterSaleAuditController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
diff --git a/supplied-service/src/dao/order.dao.ts b/supplied-service/src/dao/order.dao.ts
index b490027..e4a633f 100644
--- a/supplied-service/src/dao/order.dao.ts
+++ b/supplied-service/src/dao/order.dao.ts
@@ -137,6 +137,40 @@ export const orderDao = {
})
},
+ findMerchantSubOrders(supplierId: string, orderState?: number) {
+ return prisma.supplierOrderSub.findMany({
+ where: {
+ supplierId,
+ deleted: 0,
+ ...(Number.isInteger(orderState) ? { orderState } : {})
+ },
+ include: {
+ products: {
+ where: { deleted: 0 },
+ orderBy: { createAt: 'asc' }
+ }
+ },
+ orderBy: [
+ { createTime: 'desc' },
+ { createAt: 'desc' }
+ ]
+ })
+ },
+
+ findMerchantAfterSales(supplierId: string, serviceStates?: number[]) {
+ return prisma.supplierAfterSale.findMany({
+ where: {
+ supplierId,
+ deleted: 0,
+ ...(serviceStates?.length ? { serviceState: { in: serviceStates } } : {})
+ },
+ orderBy: [
+ { createTime: 'desc' },
+ { id: 'desc' }
+ ]
+ })
+ },
+
findUserAddresses(supplierId: string, userId: string) {
return prisma.supplierUserAddress.findMany({
where: { supplierId, userId, deleted: 0 },
diff --git a/supplied-service/src/functions/after-sale-detail.ts b/supplied-service/src/functions/after-sale-detail.ts
new file mode 100644
index 0000000..fb8cac1
--- /dev/null
+++ b/supplied-service/src/functions/after-sale-detail.ts
@@ -0,0 +1 @@
+export { afterSaleDetailController as default } from '../controllers/order.controller.js'
diff --git a/supplied-service/src/functions/merchant-after-sale-audit.ts b/supplied-service/src/functions/merchant-after-sale-audit.ts
new file mode 100644
index 0000000..e8287a9
--- /dev/null
+++ b/supplied-service/src/functions/merchant-after-sale-audit.ts
@@ -0,0 +1 @@
+export { merchantAfterSaleAuditController as default } from '../controllers/merchant.controller.js'
diff --git a/supplied-service/src/functions/merchant-after-sale-list.ts b/supplied-service/src/functions/merchant-after-sale-list.ts
new file mode 100644
index 0000000..8abb740
--- /dev/null
+++ b/supplied-service/src/functions/merchant-after-sale-list.ts
@@ -0,0 +1 @@
+export { merchantAfterSaleListController as default } from '../controllers/merchant.controller.js'
diff --git a/supplied-service/src/functions/merchant-after-sale-receive.ts b/supplied-service/src/functions/merchant-after-sale-receive.ts
new file mode 100644
index 0000000..991335a
--- /dev/null
+++ b/supplied-service/src/functions/merchant-after-sale-receive.ts
@@ -0,0 +1 @@
+export { merchantAfterSaleReceiveController as default } from '../controllers/merchant.controller.js'
diff --git a/supplied-service/src/functions/merchant-after-sale-ship.ts b/supplied-service/src/functions/merchant-after-sale-ship.ts
new file mode 100644
index 0000000..69f4ead
--- /dev/null
+++ b/supplied-service/src/functions/merchant-after-sale-ship.ts
@@ -0,0 +1 @@
+export { merchantAfterSaleShipController as default } from '../controllers/merchant.controller.js'
diff --git a/supplied-service/src/functions/merchant-order-list.ts b/supplied-service/src/functions/merchant-order-list.ts
new file mode 100644
index 0000000..83a61ff
--- /dev/null
+++ b/supplied-service/src/functions/merchant-order-list.ts
@@ -0,0 +1 @@
+export { merchantOrderListController as default } from '../controllers/merchant.controller.js'
diff --git a/supplied-service/src/functions/merchant-order-ship.ts b/supplied-service/src/functions/merchant-order-ship.ts
new file mode 100644
index 0000000..6611935
--- /dev/null
+++ b/supplied-service/src/functions/merchant-order-ship.ts
@@ -0,0 +1 @@
+export { merchantOrderShipController as default } from '../controllers/merchant.controller.js'
diff --git a/supplied-service/src/functions/upload-preview.ts b/supplied-service/src/functions/upload-preview.ts
new file mode 100644
index 0000000..a5903d7
--- /dev/null
+++ b/supplied-service/src/functions/upload-preview.ts
@@ -0,0 +1 @@
+export { uploadPreviewController as default } from '../controllers/basic.controller.js'
diff --git a/supplied-service/src/server.ts b/supplied-service/src/server.ts
index 7106ff0..25a6dbe 100644
--- a/supplied-service/src/server.ts
+++ b/supplied-service/src/server.ts
@@ -14,11 +14,13 @@ import {
cartAddController,
loginController,
uploadController,
+ uploadPreviewController,
userInfoController
} from './controllers/basic.controller.js'
import {
afterSaleApplyController,
afterSaleAuditController,
+ afterSaleDetailController,
afterSaleListController,
afterSaleLogisticsController,
orderCancelMainController,
@@ -33,6 +35,14 @@ import {
orderShipController,
supplierOrderInfoController
} from './controllers/order.controller.js'
+import {
+ merchantAfterSaleAuditController,
+ merchantAfterSaleListController,
+ merchantAfterSaleReceiveController,
+ merchantAfterSaleShipController,
+ merchantOrderListController,
+ merchantOrderShipController
+} from './controllers/merchant.controller.js'
import { productDetailController, productListController } from './controllers/product.controller.js'
import {
supplierDebugTokensController,
@@ -52,10 +62,17 @@ const handlers: Record = {
'address-save': addressSaveController,
'after-sale-apply': afterSaleApplyController,
'after-sale-audit': afterSaleAuditController,
+ 'after-sale-detail': afterSaleDetailController,
'after-sale-list': afterSaleListController,
'after-sale-logistics': afterSaleLogisticsController,
'cart-add': cartAddController,
login: loginController,
+ 'merchant-after-sale-audit': merchantAfterSaleAuditController,
+ 'merchant-after-sale-list': merchantAfterSaleListController,
+ 'merchant-after-sale-receive': merchantAfterSaleReceiveController,
+ 'merchant-after-sale-ship': merchantAfterSaleShipController,
+ 'merchant-order-list': merchantOrderListController,
+ 'merchant-order-ship': merchantOrderShipController,
'order-cancel-main': orderCancelMainController,
'order-cancel-paid-main': orderCancelPaidMainController,
'order-cancel-sub': orderCancelSubController,
@@ -74,6 +91,7 @@ const handlers: Record = {
'supplier-points': supplierPointsController,
'supplier-token': supplierTokenController,
upload: uploadController,
+ 'upload-preview': uploadPreviewController,
'user-info': userInfoController
}
@@ -116,7 +134,7 @@ const dispatch = (name: string) => async (ctx: Koa.Context) => {
await writeResponse(ctx, response)
}
-app.use(bodyParser())
+app.use(bodyParser({ jsonLimit: '10mb' }))
app.use(async (ctx, next) => {
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
if (ctx.method === 'OPTIONS') {
diff --git a/supplied-service/src/services/after-sale.service.ts b/supplied-service/src/services/after-sale.service.ts
index 946d106..d272b8c 100644
--- a/supplied-service/src/services/after-sale.service.ts
+++ b/supplied-service/src/services/after-sale.service.ts
@@ -5,6 +5,7 @@ import {
ORDER_STATE
} from '../constants/order.js'
import { orderDao } from '../dao/order.dao.js'
+import { productDao } from '../dao/product.dao.js'
import { logger } from '../utils/logger.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
@@ -17,6 +18,16 @@ export type AfterSaleApplyInput = {
serviceType?: number
amount?: number
reason?: string
+ reasonType?: string
+ problemDescription?: string
+ pickupMethod?: string
+ pickupName?: string
+ pickupMobile?: string
+ pickupAddress?: string
+ images?: unknown[]
+ exchangeProductSku?: string
+ exchangeProductName?: string
+ exchangeProductPrice?: number | string
}
export type AfterSaleListInput = {
@@ -24,6 +35,12 @@ export type AfterSaleListInput = {
userId?: string
}
+export type AfterSaleDetailInput = {
+ supplierId?: string
+ userId?: string
+ serviceOrderId?: string
+}
+
export type AfterSaleAuditInput = {
supplierId?: string
serviceOrderId?: string
@@ -111,6 +128,61 @@ const createServiceOrderId = (subOrderId: string, serviceType: number) => {
return `AS${serviceType}_${subOrderId}_${Date.now()}`
}
+const afterSaleDetailPayload = (record: NonNullable>>, subOrder?: Awaited>[number] | null) => {
+ const requestBody = record.requestBody && typeof record.requestBody === 'object' ? record.requestBody as AfterSaleApplyInput : {}
+ const products = subOrder?.products.map(product => ({
+ id: product.id,
+ sku: product.sku,
+ name: product.name,
+ number: product.number,
+ price: toYuanText(product.price),
+ image: product.productImgUrl || firstImage(product.picInfo)
+ })) || []
+
+ return {
+ serviceOrderId: record.serviceOrderId,
+ supplierId: record.supplierId,
+ userId: record.userId,
+ mainOrderId: record.mainOrderId,
+ subOrderId: record.subOrderId,
+ orderId: record.subOrderId,
+ orderNumberId: subOrder?.orderNumberId || '',
+ serviceType: record.serviceType,
+ serviceTypeText: serviceTypeText(record.serviceType),
+ serviceState: record.serviceState,
+ serviceStateText: serviceStateText(record.serviceState),
+ auditStatus: record.auditStatus,
+ logisticsState: record.logisticsState,
+ logisticsStateText: logisticsStateText(record.logisticsState),
+ amount: toYuanText(record.amount),
+ reason: record.reason || '',
+ reasonType: record.reasonType || '',
+ problemDescription: record.problemDescription || '',
+ pickupMethod: record.pickupMethod || '',
+ pickupName: record.pickupName || '',
+ pickupMobile: record.pickupMobile || '',
+ pickupAddress: record.pickupAddress || '',
+ exchangeProduct: requestBody.exchangeProductSku ? {
+ sku: requestBody.exchangeProductSku,
+ name: requestBody.exchangeProductName || '',
+ price: requestBody.exchangeProductPrice || ''
+ } : null,
+ images: Array.isArray(record.images) ? record.images : [],
+ auditRemark: record.auditRemark || '',
+ userLogisticsNo: record.userLogisticsNo || '',
+ userLogisticsCompany: record.userLogisticsCompany || '',
+ merchantLogisticsNo: record.merchantLogisticsNo || '',
+ merchantLogisticsCompany: record.merchantLogisticsCompany || '',
+ applyTime: toDateTimeText(record.applyTime || record.createTime),
+ auditTime: toDateTimeText(record.auditTime),
+ userShipTime: toDateTimeText(record.userShipTime),
+ merchantReceiveTime: toDateTimeText(record.merchantReceiveTime),
+ merchantShipTime: toDateTimeText(record.merchantShipTime),
+ finishTime: toDateTimeText(record.finishTime),
+ products
+ }
+}
+
export const afterSaleService = {
async apply(input: AfterSaleApplyInput) {
const supplierId = String(input.supplierId || '').trim()
@@ -118,6 +190,14 @@ export const afterSaleService = {
const subOrderId = String(input.subOrderId || input.orderId || '').trim()
const serviceType = Number(input.serviceType)
const reason = String(input.reason || '').trim()
+ const reasonType = String(input.reasonType || '').trim()
+ const problemDescription = String(input.problemDescription || '').trim()
+ const pickupMethod = String(input.pickupMethod || '').trim()
+ const pickupName = String(input.pickupName || '').trim()
+ const pickupMobile = String(input.pickupMobile || '').trim()
+ const pickupAddress = String(input.pickupAddress || '').trim()
+ const images = Array.isArray(input.images) ? input.images.slice(0, 3) : []
+ const exchangeProductSku = String(input.exchangeProductSku || '').trim()
logger.info('after sale apply', { supplierId, userId, subOrderId, serviceType })
if (!supplierId || !userId || !subOrderId || !serviceType) {
@@ -137,6 +217,22 @@ export const afterSaleService = {
return serviceFail('当前订单状态不能申请退款/售后', 400, 'ORDER_NOT_ALLOW_AFTER_SALE')
}
+ if (serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) {
+ if (!exchangeProductSku) {
+ return serviceFail('换货商品不能为空', 400, 'EXCHANGE_PRODUCT_REQUIRED')
+ }
+
+ const originalProduct = subOrder.products[0]
+ const exchangeProduct = await productDao.findEnabledProduct('', exchangeProductSku)
+ if (!originalProduct || !exchangeProduct) {
+ return serviceFail('换货商品不存在或已下架', 404, 'EXCHANGE_PRODUCT_NOT_FOUND')
+ }
+
+ if (toYuanText(exchangeProduct.price) !== toYuanText(originalProduct.price)) {
+ return serviceFail('换货商品价格必须与原商品价格一致', 400, 'EXCHANGE_PRODUCT_PRICE_NOT_MATCH')
+ }
+ }
+
const activeAfterSale = await orderDao.findAfterSale({
supplierId,
userId,
@@ -166,6 +262,13 @@ export const afterSaleService = {
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
amount,
reason: reason || null,
+ reasonType: reasonType || null,
+ problemDescription: problemDescription || null,
+ pickupMethod: pickupMethod || null,
+ pickupName: pickupName || null,
+ pickupMobile: pickupMobile || null,
+ pickupAddress: pickupAddress || null,
+ images,
applyTime: now,
requestBody: input
}
@@ -196,7 +299,20 @@ export const afterSaleService = {
serviceState: AFTER_SALE_SERVICE_STATE.PENDING_AUDIT,
serviceStateText: serviceStateText(AFTER_SALE_SERVICE_STATE.PENDING_AUDIT),
logisticsState: AFTER_SALE_LOGISTICS_STATE.NONE,
- logisticsStateText: logisticsStateText(AFTER_SALE_LOGISTICS_STATE.NONE)
+ logisticsStateText: logisticsStateText(AFTER_SALE_LOGISTICS_STATE.NONE),
+ reason,
+ reasonType,
+ problemDescription,
+ pickupMethod,
+ pickupName,
+ pickupMobile,
+ pickupAddress,
+ exchangeProduct: exchangeProductSku ? {
+ sku: exchangeProductSku,
+ name: input.exchangeProductName || '',
+ price: input.exchangeProductPrice || ''
+ } : null,
+ images
})
},
@@ -222,50 +338,38 @@ export const afterSaleService = {
const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
const afterSaleItems = records.map(record => {
const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
- const products = subOrder?.products.map(product => ({
- id: product.id,
- sku: product.sku,
- name: product.name,
- number: product.number,
- price: toYuanText(product.price),
- image: product.productImgUrl || firstImage(product.picInfo)
- })) || []
-
- return {
- serviceOrderId: record.serviceOrderId,
- supplierId: record.supplierId,
- userId: record.userId,
- mainOrderId: record.mainOrderId,
- subOrderId: record.subOrderId,
- orderId: record.subOrderId,
- orderNumberId: subOrder?.orderNumberId || '',
- serviceType: record.serviceType,
- serviceTypeText: serviceTypeText(record.serviceType),
- serviceState: record.serviceState,
- serviceStateText: serviceStateText(record.serviceState),
- auditStatus: record.auditStatus,
- logisticsState: record.logisticsState,
- logisticsStateText: logisticsStateText(record.logisticsState),
- amount: toYuanText(record.amount),
- reason: record.reason || '',
- auditRemark: record.auditRemark || '',
- userLogisticsNo: record.userLogisticsNo || '',
- userLogisticsCompany: record.userLogisticsCompany || '',
- merchantLogisticsNo: record.merchantLogisticsNo || '',
- merchantLogisticsCompany: record.merchantLogisticsCompany || '',
- applyTime: toDateTimeText(record.applyTime || record.createTime),
- auditTime: toDateTimeText(record.auditTime),
- userShipTime: toDateTimeText(record.userShipTime),
- merchantReceiveTime: toDateTimeText(record.merchantReceiveTime),
- merchantShipTime: toDateTimeText(record.merchantShipTime),
- finishTime: toDateTimeText(record.finishTime),
- products
- }
+ return afterSaleDetailPayload(record, subOrder)
})
return serviceOk({ afterSaleItems })
},
+ async detail(input: AfterSaleDetailInput) {
+ const supplierId = String(input.supplierId || '').trim()
+ const userId = String(input.userId || '').trim()
+ const serviceOrderId = String(input.serviceOrderId || '').trim()
+ logger.info('after sale detail query', { supplierId, userId, serviceOrderId })
+
+ if (!supplierId || !userId || !serviceOrderId) {
+ return serviceFail('供应商、用户或售后单不能为空')
+ }
+
+ const record = await orderDao.findAfterSale({
+ supplierId,
+ userId,
+ serviceOrderId,
+ deleted: 0
+ })
+ if (!record) {
+ return serviceFail('售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
+ }
+
+ const subOrders = record.subOrderId
+ ? await orderDao.findOpenApiSubOrders(supplierId, [record.subOrderId])
+ : []
+ return serviceOk({ afterSale: afterSaleDetailPayload(record, subOrders[0]) })
+ },
+
async audit(input: AfterSaleAuditInput) {
const supplierId = String(input.supplierId || '').trim()
const serviceOrderId = String(input.serviceOrderId || '').trim()
diff --git a/supplied-service/src/services/merchant.service.ts b/supplied-service/src/services/merchant.service.ts
new file mode 100644
index 0000000..981114c
--- /dev/null
+++ b/supplied-service/src/services/merchant.service.ts
@@ -0,0 +1,204 @@
+import {
+ AFTER_SALE_LOGISTICS_STATE,
+ AFTER_SALE_SERVICE_STATE,
+ ORDER_EVENT_TYPE,
+ ORDER_STATE
+} from '../constants/order.js'
+import { orderDao } from '../dao/order.dao.js'
+import { afterSaleService, type AfterSaleAuditInput, type AfterSaleLogisticsInput } from './after-sale.service.js'
+import { orderActionService } from './order-action.service.js'
+import { logger } from '../utils/logger.js'
+import { serviceFail, serviceOk } from '../types/service-result.js'
+
+export type MerchantOrderListInput = {
+ supplierId?: string
+ orderState?: number
+}
+
+export type MerchantShipInput = {
+ supplierId?: string
+ userId?: string
+ orderId?: string
+ logisticsOrderId?: string
+ crrgBsnNm?: string
+}
+
+export type MerchantAfterSaleListInput = {
+ supplierId?: string
+ scene?: string
+}
+
+const serviceStateText = (serviceState: number) => {
+ if (serviceState === AFTER_SALE_SERVICE_STATE.PENDING_AUDIT) return '商家待审核'
+ if (serviceState === AFTER_SALE_SERVICE_STATE.APPROVED) return '审核通过'
+ if (serviceState === AFTER_SALE_SERVICE_STATE.REJECTED) return '审核拒绝'
+ if (serviceState === AFTER_SALE_SERVICE_STATE.USER_SHIPPED) return '用户已寄回'
+ if (serviceState === AFTER_SALE_SERVICE_STATE.RECEIVED) return '商家已收货'
+ if (serviceState === AFTER_SALE_SERVICE_STATE.FINISHED) return '售后完成'
+ if (serviceState === AFTER_SALE_SERVICE_STATE.CANCELLED) return '已撤销'
+ return '处理中'
+}
+
+const serviceTypeText = (serviceType: number) => {
+ if (serviceType === ORDER_EVENT_TYPE.RETURN_ORDER) return '退货'
+ if (serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) return '换货'
+ return '售后'
+}
+
+const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
+ if (value && typeof value === 'object' && 'toFixed' in value) {
+ return value.toFixed(2)
+ }
+
+ return Number(value || 0).toFixed(2)
+}
+
+const toDateTimeText = (date?: Date | null) => {
+ if (!date) return ''
+ const pad = (value: number) => String(value).padStart(2, '0')
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+}
+
+const firstImage = (picInfo: unknown) => {
+ if (Array.isArray(picInfo)) {
+ return String(picInfo[0] || '')
+ }
+
+ return ''
+}
+
+const afterSaleStatesByScene = (scene: string) => {
+ if (scene === 'audit') return [AFTER_SALE_SERVICE_STATE.PENDING_AUDIT]
+ if (scene === 'process') {
+ return [
+ AFTER_SALE_SERVICE_STATE.APPROVED,
+ AFTER_SALE_SERVICE_STATE.USER_SHIPPED,
+ AFTER_SALE_SERVICE_STATE.RECEIVED
+ ]
+ }
+
+ return undefined
+}
+
+export const merchantService = {
+ async listOrders(input: MerchantOrderListInput) {
+ const supplierId = String(input.supplierId || '').trim()
+ const orderState = Number(input.orderState ?? ORDER_STATE.FINISH_PAY)
+ logger.info('merchant order list query', { supplierId, orderState })
+
+ if (!supplierId) {
+ return serviceFail('供应商不能为空')
+ }
+
+ const orders = await orderDao.findMerchantSubOrders(supplierId, orderState)
+ const orderItems = orders.map(order => ({
+ orderId: order.orderId,
+ mainOrderId: order.mainOrderId,
+ userId: order.userId || '',
+ supplierId: order.supplierId,
+ orderState: order.orderState,
+ orderNumberId: order.orderNumberId || '',
+ createTime: toDateTimeText(order.createTime || order.createAt),
+ amount: toYuanText(order.totalPrice),
+ logisticsOrderId: order.logisticsOrderId || '',
+ crrgBsnNm: order.crrgBsnNm || '',
+ products: order.products.map(product => ({
+ id: product.id,
+ sku: product.sku,
+ name: product.name,
+ number: product.number,
+ price: toYuanText(product.price),
+ image: product.productImgUrl || firstImage(product.picInfo)
+ }))
+ }))
+
+ return serviceOk({ orderItems })
+ },
+
+ shipOrder(input: MerchantShipInput) {
+ return orderActionService.shipOrder({
+ supplierId: String(input.supplierId || '').trim(),
+ userId: String(input.userId || '').trim(),
+ orderId: String(input.orderId || '').trim(),
+ deliveryStatus: 1,
+ logisticsOrderId: String(input.logisticsOrderId || '').trim(),
+ crrgBsnNm: String(input.crrgBsnNm || '').trim()
+ })
+ },
+
+ async listAfterSales(input: MerchantAfterSaleListInput) {
+ const supplierId = String(input.supplierId || '').trim()
+ const scene = String(input.scene || '').trim()
+ logger.info('merchant after sale list query', { supplierId, scene })
+
+ if (!supplierId) {
+ return serviceFail('供应商不能为空')
+ }
+
+ const records = await orderDao.findMerchantAfterSales(supplierId, afterSaleStatesByScene(scene))
+ const subOrderIds = records.map(item => item.subOrderId).filter(Boolean) as string[]
+ const subOrders = subOrderIds.length ? await orderDao.findOpenApiSubOrders(supplierId, subOrderIds) : []
+ const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
+ const afterSaleItems = records.map(record => {
+ const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
+
+ return {
+ serviceOrderId: record.serviceOrderId,
+ supplierId: record.supplierId,
+ userId: record.userId || '',
+ mainOrderId: record.mainOrderId || '',
+ subOrderId: record.subOrderId || '',
+ serviceType: record.serviceType,
+ serviceTypeText: serviceTypeText(record.serviceType),
+ serviceState: record.serviceState,
+ serviceStateText: serviceStateText(record.serviceState),
+ logisticsState: record.logisticsState,
+ reason: record.reason || '',
+ problemDescription: record.problemDescription || '',
+ pickupMethod: record.pickupMethod || '',
+ pickupName: record.pickupName || '',
+ pickupMobile: record.pickupMobile || '',
+ pickupAddress: record.pickupAddress || '',
+ userLogisticsNo: record.userLogisticsNo || '',
+ userLogisticsCompany: record.userLogisticsCompany || '',
+ applyTime: toDateTimeText(record.applyTime || record.createTime),
+ products: subOrder?.products.map(product => ({
+ id: product.id,
+ sku: product.sku,
+ name: product.name,
+ number: product.number,
+ price: toYuanText(product.price),
+ image: product.productImgUrl || firstImage(product.picInfo)
+ })) || []
+ }
+ })
+
+ return serviceOk({ afterSaleItems })
+ },
+
+ auditAfterSale(input: AfterSaleAuditInput) {
+ return afterSaleService.audit(input)
+ },
+
+ updateAfterSaleLogistics(input: AfterSaleLogisticsInput) {
+ return afterSaleService.updateLogistics(input)
+ },
+
+ receiveAfterSale(serviceOrderId: string, supplierId: string) {
+ return afterSaleService.updateLogistics({
+ supplierId,
+ serviceOrderId,
+ logisticsState: AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED
+ })
+ },
+
+ shipExchange(serviceOrderId: string, supplierId: string, logisticsNo: string, logisticsCompany: string) {
+ return afterSaleService.updateLogistics({
+ supplierId,
+ serviceOrderId,
+ logisticsState: AFTER_SALE_LOGISTICS_STATE.MERCHANT_SHIPPED,
+ merchantLogisticsNo: logisticsNo,
+ merchantLogisticsCompany: logisticsCompany
+ })
+ }
+}
diff --git a/supplied-service/src/services/minio-upload.service.ts b/supplied-service/src/services/minio-upload.service.ts
new file mode 100644
index 0000000..bbae7da
--- /dev/null
+++ b/supplied-service/src/services/minio-upload.service.ts
@@ -0,0 +1,178 @@
+import crypto from 'node:crypto'
+import { serviceFail, serviceOk } from '../types/service-result.js'
+
+export type UploadInput = {
+ fileName?: string
+ contentType?: string
+ contentBase64?: string
+ folder?: string
+}
+
+export type PreviewInput = {
+ objectName?: string
+ expires?: number
+}
+
+const minioConfig = {
+ accessKey: process.env.MINIO_ACCESS_KEY || '',
+ secretKey: process.env.MINIO_SECRET_KEY || '',
+ endPoint: process.env.MINIO_ENDPOINT || 'http://localhost:9000',
+ bucket: process.env.MINIO_BUCKET || 'njupt',
+ region: process.env.MINIO_REGION || 'us-east-1'
+}
+
+const amzDate = (date: Date) => date.toISOString().replace(/[:-]|\.\d{3}/g, '')
+const shortDate = (date: Date) => amzDate(date).slice(0, 8)
+const sha256 = (value: crypto.BinaryLike) => crypto.createHash('sha256').update(value).digest('hex')
+const hmac = (key: crypto.BinaryLike | crypto.KeyObject, value: string) => crypto.createHmac('sha256', key).update(value).digest()
+const encodePath = (value: string) => value.split('/').map(item => encodeURIComponent(item)).join('/')
+
+const ensureConfig = () => Boolean(minioConfig.accessKey && minioConfig.secretKey && minioConfig.endPoint && minioConfig.bucket)
+
+const signingKey = (date: string) => {
+ const dateKey = hmac(`AWS4${minioConfig.secretKey}`, date)
+ const regionKey = hmac(dateKey, minioConfig.region)
+ const serviceKey = hmac(regionKey, 's3')
+ return hmac(serviceKey, 'aws4_request')
+}
+
+const objectUrl = (objectName: string) => {
+ const endpoint = new URL(minioConfig.endPoint)
+ endpoint.pathname = `${endpoint.pathname.replace(/\/$/, '')}/${minioConfig.bucket}/${encodePath(objectName)}`
+ return endpoint
+}
+
+const extName = (fileName: string) => {
+ const matched = fileName.match(/\.[a-zA-Z0-9]+$/)
+ return matched ? matched[0].toLowerCase() : ''
+}
+
+const safeFolder = (folder?: string) => String(folder || 'after-sale').replace(/[^a-zA-Z0-9/_-]/g, '').replace(/^\/+|\/+$/g, '') || 'after-sale'
+
+const presign = (method: 'GET', objectName: string, expires = 3600) => {
+ const now = new Date()
+ const date = shortDate(now)
+ const fullDate = amzDate(now)
+ const credentialScope = `${date}/${minioConfig.region}/s3/aws4_request`
+ const endpoint = objectUrl(objectName)
+ const params = new URLSearchParams({
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-Credential': `${minioConfig.accessKey}/${credentialScope}`,
+ 'X-Amz-Date': fullDate,
+ 'X-Amz-Expires': String(Math.min(Math.max(Number(expires) || 3600, 1), 604800)),
+ 'X-Amz-SignedHeaders': 'host'
+ })
+ const canonicalRequest = [
+ method,
+ endpoint.pathname,
+ params.toString(),
+ `host:${endpoint.host}\n`,
+ 'host',
+ 'UNSIGNED-PAYLOAD'
+ ].join('\n')
+ const stringToSign = [
+ 'AWS4-HMAC-SHA256',
+ fullDate,
+ credentialScope,
+ sha256(canonicalRequest)
+ ].join('\n')
+ const signature = crypto.createHmac('sha256', signingKey(date)).update(stringToSign).digest('hex')
+ params.set('X-Amz-Signature', signature)
+ endpoint.search = params.toString()
+ return endpoint.toString()
+}
+
+const signedPutHeaders = (url: URL, body: Buffer, contentType: string) => {
+ const now = new Date()
+ const date = shortDate(now)
+ const fullDate = amzDate(now)
+ const payloadHash = sha256(body)
+ const credentialScope = `${date}/${minioConfig.region}/s3/aws4_request`
+ const canonicalHeaders = [
+ `content-type:${contentType}`,
+ `host:${url.host}`,
+ `x-amz-content-sha256:${payloadHash}`,
+ `x-amz-date:${fullDate}`
+ ].join('\n') + '\n'
+ const signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date'
+ const canonicalRequest = [
+ 'PUT',
+ url.pathname,
+ '',
+ canonicalHeaders,
+ signedHeaders,
+ payloadHash
+ ].join('\n')
+ const stringToSign = [
+ 'AWS4-HMAC-SHA256',
+ fullDate,
+ credentialScope,
+ sha256(canonicalRequest)
+ ].join('\n')
+ const signature = crypto.createHmac('sha256', signingKey(date)).update(stringToSign).digest('hex')
+
+ return {
+ 'Content-Type': contentType,
+ 'X-Amz-Content-Sha256': payloadHash,
+ 'X-Amz-Date': fullDate,
+ Authorization: `AWS4-HMAC-SHA256 Credential=${minioConfig.accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
+ }
+}
+
+export const minioUploadService = {
+ async upload(input: UploadInput) {
+ if (!ensureConfig()) {
+ return serviceFail('MinIO配置不完整', 500, 'MINIO_CONFIG_MISSING')
+ }
+
+ const fileName = String(input.fileName || '').trim()
+ const contentType = String(input.contentType || 'application/octet-stream').trim()
+ const contentBase64 = String(input.contentBase64 || '').replace(/^data:[^;]+;base64,/, '')
+ if (!fileName || !contentBase64) {
+ return serviceFail('文件名或图片内容不能为空')
+ }
+ if (!contentType.startsWith('image/')) {
+ return serviceFail('只支持上传图片', 400, 'ONLY_IMAGE_ALLOWED')
+ }
+
+ const body = Buffer.from(contentBase64, 'base64')
+ const objectName = `${safeFolder(input.folder)}/${Date.now()}_${crypto.randomUUID()}${extName(fileName)}`
+ const url = objectUrl(objectName)
+ const response = await fetch(url, {
+ method: 'PUT',
+ headers: signedPutHeaders(url, body, contentType),
+ body
+ })
+ if (!response.ok) {
+ return serviceFail(`图片上传失败:${response.status}`, 500, 'MINIO_UPLOAD_FAILED')
+ }
+
+ const previewUrl = presign('GET', objectName)
+ return serviceOk({
+ objectName,
+ fileName,
+ contentType,
+ size: body.length,
+ url: previewUrl,
+ previewUrl
+ })
+ },
+
+ preview(input: PreviewInput) {
+ if (!ensureConfig()) {
+ return serviceFail('MinIO配置不完整', 500, 'MINIO_CONFIG_MISSING')
+ }
+
+ const objectName = String(input.objectName || '').trim()
+ if (!objectName) {
+ return serviceFail('图片对象名不能为空')
+ }
+
+ const previewUrl = presign('GET', objectName, Number(input.expires || 3600))
+ return serviceOk({
+ objectName,
+ url: previewUrl,
+ previewUrl
+ })
+ }
+}
diff --git a/supplied-service/src/services/order-action.service.ts b/supplied-service/src/services/order-action.service.ts
index 08d74df..47bea94 100644
--- a/supplied-service/src/services/order-action.service.ts
+++ b/supplied-service/src/services/order-action.service.ts
@@ -197,6 +197,7 @@ export const orderActionService = {
return serviceFail('当前订单状态不可支付,请刷新订单状态', 400, 'ORDER_NOT_PAYABLE')
}
+ const payTime = new Date()
await orderDao.transaction(async tx => {
await tx.supplierOrderMain.update({
where: { orderId },
@@ -206,12 +207,13 @@ export const orderActionService = {
totalPrice,
wPayOrAPay,
pointsPrice,
- updateTime: new Date()
+ payTime,
+ updateTime: payTime
}
})
await tx.supplierOrderSub.updateMany({
where: { mainOrderId: orderId, deleted: 0 },
- data: { orderState: ORDER_STATE.FINISH_PAY, updateTime: new Date() }
+ data: { orderState: ORDER_STATE.FINISH_PAY, payTime, updateTime: payTime }
})
})
diff --git a/supplied-service/src/services/order-list.service.ts b/supplied-service/src/services/order-list.service.ts
index 1bdf0db..2ec9070 100644
--- a/supplied-service/src/services/order-list.service.ts
+++ b/supplied-service/src/services/order-list.service.ts
@@ -109,6 +109,7 @@ export const orderListService = {
receiverAddress: addressView?.address || '',
remark: subOrder.remark || mainOrder.remark || '',
createTime: toDateTimeText(subOrder.createTime || subOrder.createAt),
+ payTime: toDateTimeText(subOrder.payTime || mainOrder.payTime),
status: status.text,
statusClass: status.statusClass,
orderState: subOrder.orderState,
@@ -138,6 +139,7 @@ export const orderListService = {
receiverAddress: addressView?.address || '',
remark: mainOrder.remark || '',
createTime: toDateTimeText(mainOrder.createTime || mainOrder.createAt),
+ payTime: toDateTimeText(mainOrder.payTime),
status: mainStatus.text,
statusClass: mainStatus.statusClass,
orderState: mainOrder.orderState,