This commit is contained in:
XinWei 2024-10-16 15:03:29 +08:00
commit 9860ff79d5
75 changed files with 2510 additions and 909 deletions

View File

@ -0,0 +1,49 @@
import request from '@/config/axios'
// 兑换记录 VO
export interface PointOrderVO {
id: number // id
orderNumber: string // 订单号
userId: number // 用户id
productId: number // 商品id
integral: number // 兑换积分
orderStatus: number // 订单状态
orderTime: Date // 下单时间
userName: string
productName: string
imageUrl: string
}
// 兑换记录 API
export const PointOrderApi = {
// 查询兑换记录分页
getPointOrderPage: async (params: any) => {
return await request.get({ url: `/promotion/point-order/page`, params })
},
// 查询兑换记录详情
getPointOrder: async (id: number) => {
return await request.get({ url: `/promotion/point-order/get?id=` + id })
},
// 新增兑换记录
createPointOrder: async (data: PointOrderVO) => {
return await request.post({ url: `/promotion/point-order/create`, data })
},
// 修改兑换记录
updatePointOrder: async (data: PointOrderVO) => {
return await request.put({ url: `/promotion/point-order/update`, data })
},
// 删除兑换记录
deletePointOrder: async (id: number) => {
return await request.delete({ url: `/promotion/point-order/delete?id=` + id })
},
// 导出兑换记录 Excel
exportPointOrder: async (params) => {
return await request.download({ url: `/promotion/point-order/export-excel`, params })
},
}

View File

@ -22,8 +22,8 @@
<div class="ml-10px w-100%">
<div class="flex justify-between items-center w-100%">
<span class="username">{{ item.userNickname }}</span>
<span class="color-[#989EA6]">
{{ formatPast(item.lastMessageTime, 'YYYY-mm-dd') }}
<span class="color-[var(--left-menu-text-color)]" style="font-size: 13px">
{{ formatPast(item.lastMessageTime, 'YYYY-MM-DD') }}
</span>
</div>
<!-- 最后聊天内容 -->
@ -31,8 +31,9 @@
v-dompurify-html="
getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
"
class="last-message flex items-center color-[#989EA6]"
></div>
class="last-message flex items-center color-[var(--left-menu-text-color)]"
>
</div>
</div>
</div>
</div>
@ -182,7 +183,7 @@ watch(showRightMenu, (val) => {
&-conversation {
height: 60px;
padding: 10px;
background-color: #fff;
//background-color: #fff;
transition: border-left 0.05s ease-in-out; /* 设置过渡效果 */
.username {
@ -196,6 +197,7 @@ watch(showRightMenu, (val) => {
}
.last-message {
font-size: 13px;
width: 200px;
overflow: hidden; //
white-space: nowrap; //
@ -205,17 +207,17 @@ watch(showRightMenu, (val) => {
.active {
border-left: 5px #3271ff solid;
background-color: #eff0f1;
background-color: var(--login-bg-color);
}
.pinned {
background-color: #eff0f1;
background-color: var(--left-menu-bg-active-color);
}
.right-menu-ul {
position: absolute;
background-color: #fff;
padding: 10px;
background-color: var(--app-content-bg-color);
padding: 5px;
margin: 0;
list-style-type: none; /* 移除默认的项目符号 */
border-radius: 12px;
@ -228,7 +230,7 @@ watch(showRightMenu, (val) => {
border-radius: 12px;
transition: background-color 0.3s; /* 平滑过渡 */
&:hover {
background-color: #e0e0e0; /* 悬停时的背景颜色 */
background-color: var(--left-menu-bg-active-color); /* 悬停时的背景颜色 */
}
}
}

View File

@ -71,6 +71,7 @@
<MessageItem :message="item">
<ProductItem
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
:spuId="getMessageContent(item).spuId"
:picUrl="getMessageContent(item).picUrl"
:price="getMessageContent(item).price"
:skuText="getMessageContent(item).introduction"
@ -85,7 +86,7 @@
<OrderItem
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
:message="item"
class="max-w-70%"
class="max-w-100%"
/>
</MessageItem>
</div>
@ -369,9 +370,10 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
position: absolute;
bottom: 35px;
right: 35px;
background-color: #fff;
background-color: var(--app-content-bg-color);
padding: 10px;
border-radius: 30px;
font-size: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 阴影效果 */
}
@ -392,7 +394,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
border-left: 5px solid transparent;
border-bottom: 5px solid transparent;
border-top: 5px solid transparent;
border-right: 5px solid #ffffff;
border-right: 5px solid var(--app-content-bg-color);
}
}
}
@ -411,7 +413,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
right: -19px;
top: calc(50% - 10px);
position: absolute;
border-left: 5px solid #ffffff;
border-left: 5px solid var(--app-content-bg-color);
border-bottom: 5px solid transparent;
border-top: 5px solid transparent;
border-right: 5px solid transparent;
@ -421,9 +423,9 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
//
.kefu-message {
color: #333;
color: #a9a9a9;
border-radius: 5px;
box-shadow: 3px 5px 15px rgba(0, 0, 0, 0.2);
box-shadow: 3px 3px 5px rgba(220, 220, 220, 0.1);
padding: 5px 10px;
width: auto;
max-width: 50%;
@ -431,7 +433,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
display: inline-block !important;
position: relative;
word-break: break-all;
background-color: #ffffff;
background-color: var(--app-content-bg-color);
transition: all 0.2s;
&:hover {
@ -445,7 +447,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
border-radius: 12rpx;
padding: 8rpx 16rpx;
margin-bottom: 16rpx;
background-color: #e8e8e8;
//background-color: #e8e8e8;
color: #999;
font-size: 24rpx;
}
@ -453,7 +455,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
.chat-tools {
width: 100%;
border: #e4e0e0 solid 1px;
border: var(--el-border-color) solid 1px;
border-radius: 10px;
height: 44px;
}

View File

@ -7,7 +7,7 @@
<el-tab-pane label="订单列表" name="b" />
</el-tabs>
<div>
<el-scrollbar ref="scrollbarRef" always height="calc(100vh - 400px)" @scroll="handleScroll">
<el-scrollbar ref="scrollbarRef" always height="calc(115vh - 400px)" @scroll="handleScroll">
<!-- 最近浏览 -->
<ProductBrowsingHistory v-if="activeName === 'a'" ref="productBrowsingHistoryRef" />
<!-- 订单列表 -->
@ -25,7 +25,7 @@ import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { isEmpty } from '@/utils/is'
import { debounce } from 'lodash-es'
import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar'
import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar/index'
defineOptions({ name: 'MemberBrowsingHistory' })

View File

@ -1,6 +1,7 @@
<template>
<ProductItem
v-for="item in list"
:spu-id="item.spuId"
:key="item.id"
:picUrl="item.picUrl"
:price="item.price"

View File

@ -1,14 +1,20 @@
<template>
<div v-if="isObject(getMessageContent)">
<div :key="getMessageContent.id" class="order-list-card-box mt-14px">
<div class="order-card-header flex items-center justify-between p-x-20px">
<div class="order-no">订单号{{ getMessageContent.no }}</div>
<div class="order-card-header flex items-center justify-between p-x-5px">
<div class="order-no">
订单号
<span style="cursor: pointer" @click="openDetail(getMessageContent.id)">
{{ getMessageContent.no }}
</span>
</div>
<div :class="formatOrderColor(getMessageContent)" class="order-state font-16">
{{ formatOrderStatus(getMessageContent) }}
</div>
</div>
<div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
<ProductItem
:spu-id="item.spuId"
:num="item.count"
:picUrl="item.picUrl"
:price="item.price"
@ -16,7 +22,7 @@
:title="item.spuName"
/>
</div>
<div class="pay-box flex justify-end pr-20px">
<div class="pay-box flex justify-end pr-5px">
<div class="flex items-center">
<div class="discounts-title pay-color"
> {{ getMessageContent?.productCount }} 件商品,总金额:
@ -36,6 +42,8 @@ import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
import { isObject } from '@/utils/is'
import ProductItem from '@/views/mall/promotion/kefu/components/message/ProductItem.vue'
const { push } = useRouter()
defineOptions({ name: 'OrderItem' })
const props = defineProps<{
message?: KeFuMessageRespVO
@ -46,6 +54,12 @@ const getMessageContent = computed(() =>
typeof props.message !== 'undefined' ? jsonParse(props!.message!.content) : props.order
)
/** 查看订单详情 */
const openDetail = (id: number) => {
console.log(getMessageContent)
push({ name: 'TradeOrderDetail', params: { id } })
}
/**
* 格式化订单状态的颜色
*
@ -97,18 +111,28 @@ function formatOrderStatus(order: any) {
.order-list-card-box {
border-radius: 10px;
padding: 10px;
background-color: #e2e2e2;
border: 1px var(--el-border-color) solid;
background-color: var(--app-content-bg-color);
.order-card-header {
height: 28px;
.order-no {
font-size: 16px;
font-size: 12px;
font-weight: 500;
span {
&:hover {
text-decoration: underline;
color: var(--left-menu-bg-active-color);
}
}
}
}
.pay-box {
padding-top: 10px;
.discounts-title {
font-size: 16px;
line-height: normal;
@ -123,24 +147,33 @@ function formatOrderStatus(order: any) {
}
.pay-color {
color: #333;
font-size: 13px;
color: var(--left-menu-text-color);
}
}
}
.warning-color {
color: #faad14;
font-size: 11px;
font-weight: bold;
}
.danger-color {
color: #ff3000;
font-size: 11px;
font-weight: bold;
}
.success-color {
color: #52c41a;
font-size: 11px;
font-weight: bold;
}
.info-color {
color: #999999;
font-size: 11px;
font-weight: bold;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div>
<div @click.stop="openDetail(props.spuId)" style="cursor: pointer;">
<div>
<slot name="top"></slot>
</div>
@ -15,6 +15,7 @@
class="order-img"
fit="contain"
preview-teleported
@click.stop
/>
</div>
<div
@ -53,8 +54,14 @@
<script lang="ts" setup>
import { fenToYuan } from '@/utils'
const { push } = useRouter()
defineOptions({ name: 'ProductItem' })
const props = defineProps({
spuId: {
type: Number,
default: 0
},
picUrl: {
type: String,
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
@ -107,13 +114,20 @@ const skuString = computed(() => {
}
return props.skuText
})
/** 查看商品详情 */
const openDetail = (spuId: number) => {
console.log(props.spuId)
push({ name: 'ProductSpuDetail', params: { id: spuId } })
}
</script>
<style lang="scss" scoped>
.ss-order-card-warp {
padding: 20px;
border-radius: 10px;
background-color: #e2e2e2;
border: 1px var(--el-border-color) solid;
background-color: var(--app-content-bg-color);
.img-box {
width: 80px;
@ -133,19 +147,19 @@ const skuString = computed(() => {
.tool-box {
position: absolute;
right: 0px;
right: 0;
bottom: -10px;
}
}
.title-text {
font-size: 16px;
font-size: 13px;
font-weight: 500;
line-height: 20px;
}
.spec-text {
font-size: 16px;
font-size: 10px;
font-weight: 400;
color: #999999;
min-width: 0;
@ -157,13 +171,13 @@ const skuString = computed(() => {
}
.price-text {
font-size: 16px;
font-size: 11px;
font-weight: 500;
font-family: OPPOSANS;
}
.total-text {
font-size: 16px;
font-size: 10px;
font-weight: 400;
line-height: 16px;
color: #999999;

View File

@ -66,7 +66,7 @@ export const useEmoji = () => {
)
for (const path in pathList) {
const imageModule: any = await pathList[path]()
emojiPathList.value.push(imageModule.default)
emojiPathList.value.push({ path: path, src: imageModule.default })
}
}
@ -80,8 +80,8 @@ export const useEmoji = () => {
/**
*
*
* @param data
* @return
* @param content
*/
const replaceEmoji = (content: string) => {
let newData = content
@ -93,7 +93,7 @@ export const useEmoji = () => {
const emojiFile = getEmojiFileByName(item)
newData = newData.replace(
item,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}"/>`
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="${emojiFile}" alt=""/>`
)
})
}
@ -116,7 +116,10 @@ export const useEmoji = () => {
function getEmojiFileByName(name: string) {
for (const emoji of emojiList) {
if (emoji.name === name) {
return emojiPathList.value.find((item: string) => item.indexOf(emoji.file) > -1)
const emojiPath = emojiPathList.value.find(
(item: { path: string; src: string }) => item.path.indexOf(emoji.file) > -1
)
return emojiPath ? emojiPath.src : undefined
}
}
return false

View File

@ -22,119 +22,118 @@
</template>
<script lang="ts" setup>
import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
import { WebSocketMessageTypeConstants } from './components/tools/constants'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getAccessToken } from '@/utils/auth'
import { useWebSocket } from '@vueuse/core'
import { KeFuConversationList, KeFuMessageList, MemberBrowsingHistory } from './components'
import { WebSocketMessageTypeConstants } from './components/tools/constants'
import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { getRefreshToken } from '@/utils/auth'
import { useWebSocket } from '@vueuse/core'
defineOptions({ name: 'KeFu' })
defineOptions({ name: 'KeFu' })
const message = useMessage() //
const message = useMessage() //
// ======================= WebSocket start =======================
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') + '?token=' + getAccessToken()
) // WebSocket
// ======================= WebSocket start =======================
const server = ref(
(import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
'?token=' +
getRefreshToken() // 使 getRefreshToken() 使 getAccessToken() WebSocket 便访
) // WebSocket
/** 发起 WebSocket 连接 */
const { data, close, open } = useWebSocket(server.value, {
autoReconnect: true,
heartbeat: true
})
/** 发起 WebSocket 连接 */
const { data, close, open } = useWebSocket(server.value, {
autoReconnect: true,
heartbeat: true
})
/** 监听 WebSocket 数据 */
watchEffect(() => {
if (!data.value) {
console.log('111')
return
/** 监听 WebSocket 数据 */
watchEffect(() => {
if (!data.value) {
return
}
try {
// 1.
if (data.value === 'pong') {
return
}
// 2.1 type
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
if (!type) {
message.error('未知的消息类型:' + data.value)
return
}
// 2.2 KEFU_MESSAGE_TYPE
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
//
// TODO @puhui999 update
getConversationList()
//
keFuChatBoxRef.value?.refreshMessageList(JSON.parse(jsonMessage.content))
return
}
// 2.3 KEFU_MESSAGE_ADMIN_READ
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
//
// TODO @puhui999 update
getConversationList()
}
} catch (error) {
console.error(error)
}
})
// ======================= WebSocket end =======================
/** 加载会话列表 */
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
const getConversationList = () => {
keFuConversationRef.value?.getConversationList()
}
try {
// 1.
if (data.value === 'pong') {
console.log('666')
return
}
console.log('777')
// 2.1 type
const jsonMessage = JSON.parse(data.value)
const type = jsonMessage.type
if (!type) {
message.error('未知的消息类型:' + data.value)
return
}
// 2.2 KEFU_MESSAGE_TYPE
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
//
// TODO @puhui999 update
getConversationList()
//
keFuChatBoxRef.value?.refreshMessageList(JSON.parse(jsonMessage.content))
return
}
// 2.3 KEFU_MESSAGE_ADMIN_READ
if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
//
// TODO @puhui999 update
getConversationList()
}
} catch (error) {
console.error(error)
/** 加载指定会话的消息列表 */
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>()
const handleChange = (conversation: KeFuConversationRespVO) => {
keFuChatBoxRef.value?.getNewMessageList(conversation)
memberBrowsingHistoryRef.value?.initHistory(conversation)
}
})
// ======================= WebSocket end =======================
/** 加载会话列表 */
const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
const getConversationList = () => {
keFuConversationRef.value?.getConversationList()
}
/** 加载指定会话的消息列表 */
const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
const memberBrowsingHistoryRef = ref<InstanceType<typeof MemberBrowsingHistory>>()
const handleChange = (conversation: KeFuConversationRespVO) => {
keFuChatBoxRef.value?.getNewMessageList(conversation)
memberBrowsingHistoryRef.value?.initHistory(conversation)
}
/** 初始化 */
onMounted(() => {
getConversationList()
// websocket
open()
})
/** 初始化 */
onMounted(() => {
getConversationList()
// websocket
open()
})
/** 销毁 */
onBeforeUnmount(() => {
// websocket
close()
})
/** 销毁 */
onBeforeUnmount(() => {
// websocket
close()
})
</script>
<style lang="scss">
.kefu {
height: calc(100vh - 165px);
overflow: auto; /* 确保内容可滚动 */
}
.kefu {
height: calc(100vh - 165px);
overflow: auto; /* 确保内容可滚动 */
}
/* 定义滚动条样式 */
::-webkit-scrollbar {
width: 10px;
height: 6px;
}
/* 定义滚动条样式 */
::-webkit-scrollbar {
width: 10px;
height: 6px;
}
/* 定义滚动条轨道 内阴影+圆角 */
::-webkit-scrollbar-track {
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
border-radius: 10px;
background-color: #fff;
}
/* 定义滚动条轨道 内阴影+圆角 */
::-webkit-scrollbar-track {
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
border-radius: 10px;
background-color: #fff;
}
/* 定义滑块 内阴影+圆角 */
::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
background-color: rgba(240, 240, 240, 0.5);
}
/* 定义滑块 内阴影+圆角 */
::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
background-color: rgba(240, 240, 240, 0.5);
}
</style>

View File

@ -0,0 +1,123 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="订单号" prop="orderNumber">
<el-input v-model="formData.orderNumber" placeholder="请输入订单号" />
</el-form-item>
<el-form-item label="用户id" prop="userId">
<el-input v-model="formData.userId" placeholder="请输入用户" />
</el-form-item>
<el-form-item label="商品id" prop="productId">
<el-input v-model="formData.productId" placeholder="请输入商品" />
</el-form-item>
<el-form-item label="兑换积分" prop="integral">
<el-input v-model="formData.integral" placeholder="请输入兑换积分" />
</el-form-item>
<el-form-item label="订单状态" prop="orderStatus">
<el-radio-group v-model="formData.orderStatus">
<el-radio label="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="下单时间" prop="orderTime">
<el-date-picker
v-model="formData.orderTime"
type="date"
value-format="x"
placeholder="选择下单时间"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { PointOrderApi, PointOrderVO } from '@/api/mall/promotion/pointorder'
/** 兑换记录 表单 */
defineOptions({ name: 'PointOrderForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
orderNumber: undefined,
userId: undefined,
productId: undefined,
integral: undefined,
orderStatus: undefined,
orderTime: undefined,
})
const formRules = reactive({
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await PointOrderApi.getPointOrder(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as PointOrderVO
if (formType.value === 'create') {
await PointOrderApi.createPointOrder(data)
message.success(t('common.createSuccess'))
} else {
await PointOrderApi.updatePointOrder(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
orderNumber: undefined,
userId: undefined,
productId: undefined,
integral: undefined,
orderStatus: undefined,
orderTime: undefined,
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,220 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="订单号" prop="orderNumber">
<el-input v-model="queryParams.orderNumber" placeholder="请输入订单号" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<!-- <el-form-item label="用户" prop="userId">
<el-input
v-model="queryParams.userId"
placeholder="请输入用户"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="商品id" prop="productId">
<el-input
v-model="queryParams.productId"
placeholder="请输入商品id"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item> -->
<el-form-item label="兑换积分" prop="integral">
<el-input v-model="queryParams.integral" placeholder="请输入兑换积分" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item label="订单状态" prop="orderStatus">
<el-select v-model="queryParams.orderStatus" class="!w-280px" clearable placeholder="全部">
<el-option v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<!-- <el-form-item label="下单时间" prop="orderTime">
<el-date-picker
v-model="queryParams.orderTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
/>
</el-form-item> -->
<el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
<!-- <el-button type="primary" plain @click="openForm('create')"
v-hasPermi="['promotion:point-order:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button> -->
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['promotion:point-order:export']">
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<!-- <el-table-column label="ID" align="center" prop="id" /> -->
<el-table-column label="订单号" align="center" prop="orderNumber" />
<el-table-column label="用户" align="center" prop="userName" />
<el-table-column label="商品信息" align="center">
<template #default="{ row }">
<div style="display: flex; align-items: center;">
<img :src="row.imageUrl" alt="Product Image" style="width: 50px; height: 50px; margin-right: 10px;" />
<span>{{ row.productName }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="兑换积分" align="center" prop="integral" />
<!-- <el-table-column label="订单状态" align="center" prop="orderStatus" /> -->
<el-table-column label="订单状态" align="center" prop="orderStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="scope.row.orderStatus" />
</template>
</el-table-column>
<el-table-column label="下单时间" align="center" prop="orderTime" :formatter="dateFormatter" width="180px" />
<!-- <el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/> -->
<el-table-column label="操作" align="center">
<template #default="scope">
<!-- <el-button link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['promotion:point-order:update']">
编辑
</el-button> -->
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['promotion:point-order:delete']">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<PointOrderForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { PointOrderApi, PointOrderVO } from '@/api/mall/promotion/pointorder'
import PointOrderForm from './PointOrderForm.vue'
/** 兑换记录 列表 */
defineOptions({ name: 'PointOrder' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<PointOrderVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
orderNumber: undefined,
userId: undefined,
productId: undefined,
integral: undefined,
orderStatus: undefined,
orderTime: [],
createTime: [],
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await PointOrderApi.getPointOrderPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type : string, id ?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id : number) => {
try {
//
await message.delConfirm()
//
await PointOrderApi.deletePointOrder(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch { }
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await PointOrderApi.exportPointOrder(queryParams)
download.excel(data, '兑换记录.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
<style>
</style>

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants;
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
@ -151,7 +152,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
*/
default Boolean insertBatch(Collection<T> entities) {
// 特殊SQL Server 批量插入后获取 id 会报错因此通过循环处理
if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) {
DbType dbType = JdbcUtils.getDbType();
if (JdbcUtils.isSQLServer(dbType)) {
entities.forEach(this::insert);
return CollUtil.isNotEmpty(entities);
}

View File

@ -1,9 +1,11 @@
package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.annotation.DbType;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import javax.sql.DataSource;
import java.sql.Connection;
@ -43,14 +45,28 @@ public class JdbcUtils {
return com.baomidou.mybatisplus.extension.toolkit.JdbcUtils.getDbType(url);
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param dbType DB 类型
* @return 是否为 SQLServer 数据库
*/
public static boolean isSQLServer(DbType dbType) {
return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}
/**
* 通过当前数据库连接获得对应的 DB 类型
*
* @return DB 类型
*/
public static DbType getDbType() {
DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
DataSource dataSource = dynamicRoutingDataSource.determineDataSource();
DataSource dataSource;
try {
DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
dataSource = dynamicRoutingDataSource.determineDataSource();
} catch (NoSuchBeanDefinitionException e) {
dataSource = SpringUtils.getBean(DataSource.class);
}
try (Connection conn = dataSource.getConnection()) {
return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName());
} catch (SQLException e) {

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import lombok.Data;
import java.util.List;
// TODO @LeeYan9: ProductSpuRespDTO
/**
* 商品 SPU 信息 Response DTO
@ -69,6 +71,13 @@ public class ProductSpuRespDTO {
// ========== 物流相关字段 =========
/**
* 配送方式数组
*
* 对应 DeliveryTypeEnum 枚举
*/
private List<Integer> deliveryTypes;
/**
* 物流配置模板编号
*

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import javax.validation.Valid;
import java.util.List;
/**
* 优惠劵 API 接口
@ -20,6 +21,15 @@ public interface CouponApi {
*/
void useCoupon(@Valid CouponUseReqDTO useReqDTO);
/**
* 获得用户的优惠劵列表
*
* @param userId 用户编号
* @param status 优惠劵状态
* @return 优惠劵列表
*/
List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status);
/**
* 退还已使用的优惠券
*

View File

@ -27,4 +27,6 @@ public interface RewardActivityApi {
*/
RewardActivityDTO getRewardActivityById(Long id);
}

View File

@ -1,9 +1,14 @@
package cn.iocoder.yudao.module.promotion.api.reward.dto;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionConditionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 满减送活动的匹配 Response DTO
@ -13,6 +18,12 @@ import java.util.List;
@Data
public class RewardActivityMatchRespDTO {
/**
* 匹配的 SPU 数组
*/
private List<Long> spuIds;
/**
* 活动编号主键自增
*/
@ -21,28 +32,50 @@ public class RewardActivityMatchRespDTO {
* 活动标题
*/
private String name;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 备注
*/
private String remark;
/**
* 条件类型
*
* 枚举 {@link PromotionConditionTypeEnum}
*/
private Integer conditionType;
/**
* 商品范围
*
* 枚举 {@link PromotionProductScopeEnum}
*/
private Integer productScope;
/**
* 商品 SPU 编号的数组
*/
private List<Long> productScopeValues;
/**
* 优惠规则的数组
*/
private List<Rule> rules;
/**
* 商品 SPU 编号的数组
*/
private List<Long> spuIds;
// TODO 芋艿后面 RewardActivityRespDTO 有了之后Rule 可以放过去
/**
* 优惠规则
*/
@Data
public static class Rule {
public static class Rule implements Serializable {
/**
* 优惠门槛
@ -64,13 +97,21 @@ public class RewardActivityMatchRespDTO {
*/
private Integer point;
/**
* 赠送的优惠劵编号的数组
* 赠送的优惠劵
*
* key: 优惠劵模版编号
* value对应的优惠券数量
*
* 目的用于订单支付后赠送优惠券
*/
private List<Long> couponIds;
private Map<Long, Integer> giveCouponTemplateCounts;
/**
* 赠送的优惠券数量的数组
* 规则描述
*
* 通过 {@link #limit}{@link #discountPrice} 等字段进行拼接
*/
private List<Integer> couponCounts;
private String description;
}

View File

@ -16,6 +16,9 @@ public interface ErrorCodeConstants {
ErrorCode DISCOUNT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1_013_001_003, "限时折扣活动未关闭,不能删除");
ErrorCode DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1_013_001_004, "限时折扣活动已关闭,不能重复关闭");
// ========== 兑换记录 TODO 补充编号 ==========
ErrorCode POINT_ORDER_NOT_EXISTS = new ErrorCode(11111, "兑换记录不存在");
// ========== Banner 相关 1-013-002-000 ============
ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1_013_002_000, "Banner 不存在");

View File

@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 营销的商品范围枚举
@ -15,10 +16,9 @@ import java.util.Arrays;
@AllArgsConstructor
public enum PromotionProductScopeEnum implements IntArrayValuable {
ALL(1, "通用券"), // 全部商品
SPU(2, "商品券"), // 指定商品
CATEGORY(3, "品类券"), // 指定品类
;
ALL(1, "全部商品"),
SPU(2, "指定商品"),
CATEGORY(3, "指定品类");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray();
@ -36,4 +36,17 @@ public enum PromotionProductScopeEnum implements IntArrayValuable {
return ARRAYS;
}
public static boolean isAll(Integer scope) {
return Objects.equals(scope, ALL.scope);
}
public static boolean isSpu(Integer scope) {
return Objects.equals(scope, SPU.scope);
}
public static boolean isCategory(Integer scope) {
return Objects.equals(scope, CATEGORY.scope);
}
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.promotion.api.coupon;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponUseReqDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
@ -11,6 +12,7 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.util.List;
/**
* 优惠劵 API 实现类
@ -24,6 +26,11 @@ public class CouponApiImpl implements CouponApi {
@Resource
private CouponService couponService;
@Override
public List<CouponRespDTO> getCouponListByUserId(Long userId, Integer status) {
return BeanUtils.toBean(couponService.getCouponList(userId, status), CouponRespDTO.class);
}
@Override
public void useCoupon(CouponUseReqDTO useReqDTO) {
couponService.useCoupon(useReqDTO.getId(), useReqDTO.getUserId(),

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.promotion.api.discount;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
import cn.iocoder.yudao.module.promotion.convert.discount.DiscountActivityConvert;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import cn.iocoder.yudao.module.promotion.service.discount.DiscountActivityService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@ -24,7 +26,7 @@ public class DiscountActivityApiImpl implements DiscountActivityApi {
@Override
public List<DiscountProductRespDTO> getMatchDiscountProductList(Collection<Long> skuIds) {
return DiscountActivityConvert.INSTANCE.convertList02(discountActivityService.getMatchDiscountProductList(skuIds));
}
List<DiscountProductDO> list = discountActivityService.getMatchDiscountProductList(skuIds);
return BeanUtils.toBean(list, DiscountProductRespDTO.class); }
}

View File

@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.promotion.controller.admin.pointorder;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import javax.validation.*;
import javax.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.promotion.controller.admin.pointorder.vo.*;
import cn.iocoder.yudao.module.promotion.dal.dataobject.pointorder.PointOrderDO;
import cn.iocoder.yudao.module.promotion.service.pointorder.PointOrderService;
@Tag(name = "管理后台 - 兑换记录")
@RestController
@RequestMapping("/promotion/point-order")
@Validated
public class PointOrderController {
@Resource
private PointOrderService pointOrderService;
@PostMapping("/create")
@Operation(summary = "创建兑换记录")
@PreAuthorize("@ss.hasPermission('promotion:point-order:create')")
public CommonResult<Long> createPointOrder(@Valid @RequestBody PointOrderSaveReqVO createReqVO) {
return success(pointOrderService.createPointOrder(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新兑换记录")
@PreAuthorize("@ss.hasPermission('promotion:point-order:update')")
public CommonResult<Boolean> updatePointOrder(@Valid @RequestBody PointOrderSaveReqVO updateReqVO) {
pointOrderService.updatePointOrder(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除兑换记录")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('promotion:point-order:delete')")
public CommonResult<Boolean> deletePointOrder(@RequestParam("id") Long id) {
pointOrderService.deletePointOrder(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得兑换记录")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('promotion:point-order:query')")
public CommonResult<PointOrderRespVO> getPointOrder(@RequestParam("id") Long id) {
PointOrderDO pointOrder = pointOrderService.getPointOrder(id);
return success(BeanUtils.toBean(pointOrder, PointOrderRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得兑换记录分页")
@PreAuthorize("@ss.hasPermission('promotion:point-order:query')")
public CommonResult<PageResult<PointOrderRespVO>> getPointOrderPage(@Valid PointOrderPageReqVO pageReqVO) {
PageResult<PointOrderDO> pageResult = pointOrderService.getPointOrderPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, PointOrderRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出兑换记录 Excel")
@PreAuthorize("@ss.hasPermission('promotion:point-order:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportPointOrderExcel(@Valid PointOrderPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<PointOrderDO> list = pointOrderService.getPointOrderPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "兑换记录.xls", "数据", PointOrderRespVO.class,
BeanUtils.toBean(list, PointOrderRespVO.class));
}
}

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.promotion.controller.admin.pointorder.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 兑换记录分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PointOrderPageReqVO extends PageParam {
@Schema(description = "订单号")
private String orderNumber;
@Schema(description = "用户id", example = "13639")
private Long userId;
@Schema(description = "商品id", example = "2315")
private Long productId;
@Schema(description = "兑换积分")
private Integer integral;
@Schema(description = "订单状态", example = "2")
private Integer orderStatus;
@Schema(description = "下单时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] orderTime;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,53 @@
package cn.iocoder.yudao.module.promotion.controller.admin.pointorder.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import com.alibaba.excel.annotation.*;
@Schema(description = "管理后台 - 兑换记录 Response VO")
@Data
@ExcelIgnoreUnannotated
public class PointOrderRespVO {
@Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "19849")
@ExcelProperty("id")
private Long id;
@Schema(description = "订单号")
@ExcelProperty("订单号")
private String orderNumber;
@Schema(description = "用户id", example = "13639")
@ExcelProperty("用户id")
private Long userId;
@Schema(description = "商品id", example = "2315")
@ExcelProperty("商品id")
private Long productId;
@Schema(description = "兑换积分")
@ExcelProperty("兑换积分")
private Integer integral;
@Schema(description = "订单状态", example = "2")
@ExcelProperty("订单状态")
private Integer orderStatus;
@Schema(description = "下单时间")
@ExcelProperty("下单时间")
private LocalDateTime orderTime;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
private String userName;
private String productName;
private String imageUrl;
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.promotion.controller.admin.pointorder.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import javax.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 兑换记录新增/修改 Request VO")
@Data
public class PointOrderSaveReqVO {
@Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "19849")
private Long id;
@Schema(description = "订单号")
private String orderNumber;
@Schema(description = "用户id", example = "13639")
private Long userId;
@Schema(description = "商品id", example = "2315")
private Long productId;
@Schema(description = "兑换积分")
private Integer integral;
@Schema(description = "订单状态", example = "2")
private Integer orderStatus;
@Schema(description = "下单时间")
private LocalDateTime orderTime;
}

View File

@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.promotion.dal.dataobject.pointorder;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 兑换记录 DO
*
* @author 管理员
*/
@TableName("promotion_point_order")
@KeySequence("promotion_point_order_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PointOrderDO extends BaseDO {
/**
* id
*/
@TableId
private Long id;
/**
* 订单号
*/
private String orderNumber;
/**
* 用户id
*/
private Long userId;
/**
* 商品id
*/
private Long productId;
/**
* 兑换积分
*/
private Integer integral;
/**
* 订单状态
*/
private Integer orderStatus;
/**
* 下单时间
*/
private LocalDateTime orderTime;
/**
* 用户名称
*/
@TableField(exist = false)
private String userName;
/**
* 商品名称
*/
@TableField(exist = false)
private String productName;
@TableField(exist = false)
private String imageUrl;
}

View File

@ -1,11 +1,13 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.discount;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.promotion.dal.dataobject.discount.DiscountProductDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@ -22,6 +24,15 @@ public interface DiscountProductMapper extends BaseMapperX<DiscountProductDO> {
return selectList(DiscountProductDO::getSkuId, skuIds);
}
default List<DiscountProductDO> selectListBySkuIdsAndStatusAndNow(Collection<Long> skuIds, Integer status) {
LocalDateTime now = LocalDateTime.now();
return selectList(new LambdaQueryWrapperX<DiscountProductDO>()
.in(DiscountProductDO::getSkuId, skuIds)
.eq(DiscountProductDO::getActivityStatus,status)
.lt(DiscountProductDO::getActivityStartTime, now)
.gt(DiscountProductDO::getActivityEndTime, now));
}
default List<DiscountProductDO> selectListByActivityId(Long activityId) {
return selectList(DiscountProductDO::getActivityId, activityId);
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.pointorder;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.promotion.dal.dataobject.pointorder.PointOrderDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.promotion.controller.admin.pointorder.vo.*;
/**
* 兑换记录 Mapper
*
* @author 管理员
*/
@Mapper
public interface PointOrderMapper extends BaseMapperX<PointOrderDO> {
default PageResult<PointOrderDO> selectPage(PointOrderPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PointOrderDO>()
.eqIfPresent(PointOrderDO::getOrderNumber, reqVO.getOrderNumber())
.eqIfPresent(PointOrderDO::getUserId, reqVO.getUserId())
.eqIfPresent(PointOrderDO::getProductId, reqVO.getProductId())
.eqIfPresent(PointOrderDO::getIntegral, reqVO.getIntegral())
.eqIfPresent(PointOrderDO::getOrderStatus, reqVO.getOrderStatus())
.betweenIfPresent(PointOrderDO::getOrderTime, reqVO.getOrderTime())
.betweenIfPresent(PointOrderDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PointOrderDO::getId));
}
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.promotion.service.pointorder;
import java.util.*;
import javax.validation.*;
import cn.iocoder.yudao.module.promotion.controller.admin.pointorder.vo.*;
import cn.iocoder.yudao.module.promotion.dal.dataobject.pointorder.PointOrderDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* 兑换记录 Service 接口
*
* @author 管理员
*/
public interface PointOrderService {
/**
* 创建兑换记录
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createPointOrder(@Valid PointOrderSaveReqVO createReqVO);
/**
* 更新兑换记录
*
* @param updateReqVO 更新信息
*/
void updatePointOrder(@Valid PointOrderSaveReqVO updateReqVO);
/**
* 删除兑换记录
*
* @param id 编号
*/
void deletePointOrder(Long id);
/**
* 获得兑换记录
*
* @param id 编号
* @return 兑换记录
*/
PointOrderDO getPointOrder(Long id);
/**
* 获得兑换记录分页
*
* @param pageReqVO 分页查询
* @return 兑换记录分页
*/
PageResult<PointOrderDO> getPointOrderPage(PointOrderPageReqVO pageReqVO);
}

View File

@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.promotion.service.pointorder;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import cn.iocoder.yudao.module.promotion.controller.admin.pointorder.vo.*;
import cn.iocoder.yudao.module.promotion.dal.dataobject.pointorder.PointOrderDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.dal.mysql.pointorder.PointOrderMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
/**
* 兑换记录 Service 实现类
*
* @author 管理员
*/
@Service
@Validated
public class PointOrderServiceImpl implements PointOrderService {
@Resource
private PointOrderMapper pointOrderMapper;
@Resource
private MemberUserApi memberUserApi;
@Resource
private ProductSpuApi productSpuApi;
@Override
public Long createPointOrder(PointOrderSaveReqVO createReqVO) {
// 插入
PointOrderDO pointOrder = BeanUtils.toBean(createReqVO, PointOrderDO.class);
pointOrderMapper.insert(pointOrder);
// 返回
return pointOrder.getId();
}
@Override
public void updatePointOrder(PointOrderSaveReqVO updateReqVO) {
// 校验存在
validatePointOrderExists(updateReqVO.getId());
// 更新
PointOrderDO updateObj = BeanUtils.toBean(updateReqVO, PointOrderDO.class);
pointOrderMapper.updateById(updateObj);
}
@Override
public void deletePointOrder(Long id) {
// 校验存在
validatePointOrderExists(id);
// 删除
pointOrderMapper.deleteById(id);
}
private void validatePointOrderExists(Long id) {
if (pointOrderMapper.selectById(id) == null) {
throw exception(POINT_ORDER_NOT_EXISTS);
}
}
@Override
public PointOrderDO getPointOrder(Long id) {
return pointOrderMapper.selectById(id);
}
@Override
public PageResult<PointOrderDO> getPointOrderPage(PointOrderPageReqVO pageReqVO) {
PageResult<PointOrderDO> pointOrderDOPageResult = pointOrderMapper.selectPage(pageReqVO);
List<PointOrderDO> list = pointOrderDOPageResult.getList();
for (int i = 0; i < list.size(); i++) {
PointOrderDO pointOrderDO = list.get(i);
//设置用户名称
MemberUserRespDTO user = memberUserApi.getUser(pointOrderDO.getUserId());
pointOrderDO.setUserName(user.getNickname());
//获取商品信息
ProductSpuRespDTO spu = productSpuApi.getSpu(pointOrderDO.getProductId());
pointOrderDO.setProductName(spu.getName());
pointOrderDO.setImageUrl(spu.getPicUrl());
}
return pointOrderDOPageResult;
}
}

View File

@ -35,6 +35,7 @@ public interface ErrorCodeConstants {
ErrorCode ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP = new ErrorCode(1_011_000_030, "交易订单自提失败,收货方式不是【用户自提】");
ErrorCode ORDER_UPDATE_ADDRESS_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1_011_000_031, "交易订单修改收货地址失败,原因:订单不是【待发货】状态");
ErrorCode ORDER_CREATE_FAIL_EXIST_UNPAID = new ErrorCode(1_011_000_032, "交易订单创建失败,原因:存在未付款订单");
ErrorCode ORDER_CANCEL_PAID_FAIL = new ErrorCode(1_011_000_033, "交易订单取消支付失败,原因:订单不是【{}】状态");
// ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");
@ -53,12 +54,15 @@ public interface ErrorCodeConstants {
// ========== Cart 模块 1-011-002-000 ==========
ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1_011_002_000, "购物车项不存在");
ErrorCode PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_004, "参与积分活动的商品,超过了积分活动商品总限购数量");
// ========== Price 相关 1-011-003-000 ============
ErrorCode PRICE_CALCULATE_PAY_PRICE_ILLEGAL = new ErrorCode(1_011_003_000, "支付价格计算异常,原因:价格小于等于 0");
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1_011_003_002, "计算快递运费异常,找不到对应的运费模板");
ErrorCode PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER = new ErrorCode(1_011_003_004, "参与秒杀、拼团、砍价的营销商品,无法使用优惠劵");
ErrorCode PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT = new ErrorCode(1_011_003_005, "参与秒杀的商品,超过了秒杀总限购数量");
ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL = new ErrorCode(1_011_003_005, "计算快递运费异常,配送方式不匹配");
ErrorCode PRICE_CALCULATE_COUPON_CAN_NOT_USE = new ErrorCode(1_011_003_006, "该优惠劵无法使用,原因:{}」");
// ========== 物流 Express 模块 1-011-004-000 ==========
ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1_011_004_000, "快递公司不存在");

View File

@ -12,5 +12,7 @@ public interface MessageTemplateConstants {
String BROKERAGE_WITHDRAW_AUDIT_APPROVE = "brokerage_withdraw_audit_approve"; // 佣金提现审核通过
String BROKERAGE_WITHDRAW_AUDIT_REJECT = "brokerage_withdraw_audit_reject"; // 佣金提现审核不通过
// ======================= 小程序订阅消息模版 =======================
String WXA_ORDER_DELIVERY = "订单发货通知";
}

View File

@ -17,8 +17,8 @@ public enum TradeOrderCancelTypeEnum implements IntArrayValuable {
PAY_TIMEOUT(10, "超时未支付"),
AFTER_SALE_CLOSE(20, "退款关闭"),
MEMBER_CANCEL(30, "买家取消");
MEMBER_CANCEL(30, "买家取消"),
COMBINATION_CLOSE(40, "拼团关闭");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray();
/**

View File

@ -15,11 +15,11 @@ import java.util.Arrays;
@RequiredArgsConstructor
@Getter
public enum TradeOrderTypeEnum implements IntArrayValuable {
NORMAL(0, "普通订单"),
SECKILL(1, "秒杀订单"),
BARGAIN(2, "砍价订单"),
COMBINATION(3, "拼团订单"),
POINT(4, "积分商城"),
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderTypeEnum::getType).toArray();
@ -54,4 +54,8 @@ public enum TradeOrderTypeEnum implements IntArrayValuable {
return ObjectUtil.equal(type, COMBINATION.getType());
}
public static boolean isPoint(Integer type) {
return ObjectUtil.equal(type, POINT.getType());
}
}

View File

@ -92,6 +92,11 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-promotion-biz</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@ -1,33 +0,0 @@
### 获得交易售后分页 => 成功
GET {{baseUrl}}/trade/after-sale/page?pageNo=1&pageSize=10
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
### 同意售后 => 成功
PUT {{baseUrl}}/trade/after-sale/agree?id=7
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
Content-Type: application/json
### 拒绝售后 => 成功
PUT {{baseUrl}}/trade/after-sale/disagree
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
Content-Type: application/json
{
"id": 6,
"auditReason": "阿巴巴"
}
### 确认退款 => 成功
PUT {{baseUrl}}/trade/after-sale/refund?id=6
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
Content-Type: application/json
### 确认收货 => 成功
PUT {{baseUrl}}/trade/after-sale/receive?id=7
Authorization: Bearer {{token}}
tenant-id: {{adminTenentId}}
Content-Type: application/json

View File

@ -1,8 +1,6 @@
package cn.iocoder.yudao.module.trade.controller.admin.order;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
@ -12,13 +10,9 @@ import cn.iocoder.yudao.module.trade.convert.order.TradeOrderConvert;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderLogDO;
import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderLogService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -28,11 +22,10 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -121,94 +114,6 @@ public class TradeOrderController {
return success(true);
}
@PostMapping("/test")
public CommonResult<String> test(@RequestBody Map<String, String> map){
System.out.println("==========================================================");
for (String s : map.keySet()) {
System.out.println(map.get(s));
}
System.out.println("==========================================================");
return success("成功");
}
@PostMapping("/test2")
public void test2(){
//获取access_token
String accessTokenApiurl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wx63c280fe3248a3e7&secret=6f270509224a7ae1296bbf1c8cb97aed";
// JSONObject map = new JSONObject();
// map.put("grant_type","client_credential");
// map.put("appid","wx63c280fe3248a3e7");
// map.put("secret","6f270509224a7ae1296bbf1c8cb97aed");
HttpResponse get = HttpUtil.createGet(accessTokenApiurl).execute();
String body = get.body();
System.out.println("-----------------------------------------------");
// String jsonString = "82_fCctY_2YxIDOQMpk315ZH76M0J49nXlx_aEdbrDfJEF_-E640cLl0_Yu3kAh4eHgAgtTpu2SskXMNFCsraCBvlGlEOKsXtpqEbkrh5Ydi2arCv-MoHWiS45UyzEMDYfAJAYPK";
JSONObject jsonObject = JSON.parseObject(body);
System.out.println(jsonObject);
System.out.println(body);
String token = jsonObject.getString("access_token");
System.out.println(token);
System.out.println("-----------------------------------------------");
//查询小程序是否已开通发货信息管理服务
String apiurlSec = "https://api.weixin.qq.com/wxa/sec/order/is_trade_managed?access_token="+token;
JSONObject mapSec = new JSONObject();
mapSec.put("appid","wx63c280fe3248a3e7");
HttpResponse postSec = HttpUtil.createPost(apiurlSec)
// .header("Authorization","Bearer 8e79d003102a4b5c80fd823c3c04347c")
// .header("tenant-id","1")
.body(mapSec.toJSONString())
.execute();
String bodySec = postSec.body();
System.out.println("-----------------------------------------------6");
System.out.println(bodySec);
System.out.println("-----------------------------------------------6");
// 微信官方发货信息录入接口
// String apiurl = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token="+token;
//
// JSONObject map = new JSONObject();
// map.put("order_id","P202407170517541");
//
// JSONObject orderKey = new JSONObject();
// orderKey.put("order_number_type",1);
//
// map.put("order_key",orderKey);
// map.put("logistics_type",3);
// map.put("delivery_mode",1);
//
// JSONArray shippingList = new JSONArray();
// JSONObject object = new JSONObject();
// object.put("item_desc","测试商品666");
// shippingList.add(object);
//
// map.put("shipping_list",shippingList);
// // 获取当前时间的 OffsetDateTime 对象
// OffsetDateTime currentDateTime = OffsetDateTime.now();
//
// // 定义日期时间格式化器用于格式化成特定格式
// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
//
// // 格式化 OffsetDateTime 对象为指定格式的字符串
// String formattedDateTime = currentDateTime.format(formatter);
// map.put("upload_time",formattedDateTime);
// JSONObject payer = new JSONObject();
// payer.put("openid","oWKZ063Eb6IGr63vJT9Zgf3jufOY");
// map.put("payer",payer);
//
// HttpResponse post = HttpUtil.createPost(apiurl)
//// .header("Authorization","Bearer 8e79d003102a4b5c80fd823c3c04347c")
//// .header("tenant-id","1")
// .body(map.toJSONString())
// .execute();
// String Pbody = post.body();
// System.out.println("-----------------------------------------------");
// System.out.println(Pbody);
// System.out.println(apiurl);
// System.out.println("-----------------------------------------------");
}
@PutMapping("/update-remark")
@Operation(summary = "订单备注")
@PreAuthorize("@ss.hasPermission('trade:order:update')")

View File

@ -1,42 +0,0 @@
### 请求 /trade/cart/add 接口 => 成功
POST {{appApi}}/trade/cart/add
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
Content-Type: application/json
{
"skuId": 1,
"count": 10,
"addStatus": true
}
### 请求 /trade/cart/update 接口 => 成功
PUT {{appApi}}/trade/cart/update
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
Content-Type: application/json
{
"id": 35,
"count": 5
}
### 请求 /trade/cart/delete 接口 => 成功
DELETE {{appApi}}/trade/cart/delete?ids=1
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
### 请求 /trade/cart/get-count 接口 => 成功
GET {{appApi}}/trade/cart/get-count
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
### 请求 /trade/cart/get-count-map 接口 => 成功
GET {{appApi}}/trade/cart/get-count-map
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}
### 请求 /trade/cart/list 接口 => 成功
GET {{appApi}}/trade/cart/list
tenant-id: {{appTenentId}}
Authorization: Bearer {{appToken}}

View File

@ -62,3 +62,8 @@ tenant-id: {{appTenentId}}
GET {{appApi}}/trade/order/get-express-track-list?id=70
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}
### /trade-order/settlement-product 获得商品结算信息
GET {{appApi}}/trade/order/settlement-product?spuIds=633
Authorization: Bearer {{appToken}}
tenant-id: {{appTenentId}}

View File

@ -1,12 +1,7 @@
package cn.iocoder.yudao.module.trade.controller.app.order;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.member.api.config.MemberConfigApi;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.*;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO;
@ -21,17 +16,18 @@ import cn.iocoder.yudao.module.trade.service.aftersale.AfterSaleService;
import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import cn.iocoder.yudao.module.trade.service.price.TradePriceService;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
@ -53,28 +49,24 @@ public class AppTradeOrderController {
private TradeOrderQueryService tradeOrderQueryService;
@Resource
private DeliveryExpressService deliveryExpressService;
@Resource
private AfterSaleService afterSaleService;
@Resource
private TradePriceService priceService;
@Resource
private TradeOrderProperties tradeOrderProperties;
@Resource
private MemberUserApi memberUserApi;
@GetMapping("/settlement")
@Operation(summary = "获得订单结算信息")
@PreAuthenticated
public CommonResult<AppTradeOrderSettlementRespVO> settlementOrder(@Valid AppTradeOrderSettlementReqVO settlementReqVO) {
AppTradeOrderSettlementRespVO order = tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO);
order.setTotalPoint(memberUserApi.getUser(getLoginUserId()).getPoint());
order.setUsedPoint(0);
return success(order);
return success(tradeOrderUpdateService.settlementOrder(getLoginUserId(), settlementReqVO));
}
@PostMapping("/create")
@Operation(summary = "创建订单")
@PreAuthenticated
public CommonResult<AppTradeOrderCreateRespVO> createOrder(@Valid @RequestBody AppTradeOrderCreateReqVO createReqVO) {
TradeOrderDO order = tradeOrderUpdateService.createOrder(getLoginUserId(), createReqVO);
return success(new AppTradeOrderCreateRespVO().setId(order.getId()).setPayOrderId(order.getPayOrderId()));
@ -82,6 +74,7 @@ public class AppTradeOrderController {
@PostMapping("/update-paid")
@Operation(summary = "更新订单为已支付") // pay-module 支付服务进行回调可见 PayNotifyJob
@PermitAll
public CommonResult<Boolean> updateOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) {
tradeOrderUpdateService.updateOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()),
notifyReqDTO.getPayOrderId());
@ -90,20 +83,31 @@ public class AppTradeOrderController {
@GetMapping("/get-detail")
@Operation(summary = "获得交易订单")
@Parameter(name = "id", description = "交易订单编号")
public CommonResult<AppTradeOrderDetailRespVO> getOrder(@RequestParam("id") Long id) {
// 查询订单
@Parameters({
@Parameter(name = "id", description = "交易订单编号"),
@Parameter(name = "sync", description = "是否同步支付状态", example = "true")
})
public CommonResult<AppTradeOrderDetailRespVO> getOrderDetail(@RequestParam("id") Long id,
@RequestParam(value = "sync", required = false) Boolean sync) {
// 1.1 查询订单
TradeOrderDO order = tradeOrderQueryService.getOrder(getLoginUserId(), id);
if (order == null) {
return success(null);
}
// 1.2 sync 仅在等待支付
if (Boolean.TRUE.equals(sync)
&& TradeOrderStatusEnum.isUnpaid(order.getStatus()) && !order.getPayStatus()) {
tradeOrderUpdateService.syncOrderPayStatusQuietly(order.getId(), order.getPayOrderId());
// 重新查询因为同步后可能会有变化
order = tradeOrderQueryService.getOrder(id);
}
// 查询订单项
// 2.1 查询订单项
List<TradeOrderItemDO> orderItems = tradeOrderQueryService.getOrderItemListByOrderId(order.getId());
// 查询物流公司
// 2.2 查询物流公司
DeliveryExpressDO express = order.getLogisticsId() != null && order.getLogisticsId() > 0 ?
deliveryExpressService.getDeliveryExpress(order.getLogisticsId()) : null;
// 最终组合
// 2.3 最终组合
return success(TradeOrderConvert.INSTANCE.convert02(order, orderItems, tradeOrderProperties, express));
}
@ -158,32 +162,6 @@ public class AppTradeOrderController {
return success(true);
}
// @PostMapping("/test")
// public void test(){
// //获取access_token
// String accessTokenApiurl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wx63c280fe3248a3e7&secret=6f270509224a7ae1296bbf1c8cb97aed";
// HttpResponse get = HttpUtil.createGet(accessTokenApiurl).execute();
// String body = get.body();
// System.out.println("-----------------------------------------------");
// JSONObject jsonObject = JSON.parseObject(body);
// System.out.println(jsonObject);
// System.out.println(body);
// String token = jsonObject.getString("access_token");
// System.out.println(token);
// System.out.println("-----------------------------------------------");
//
// String apiurlSec = "https://api.weixin.qq.com/wxa/sec/order/is_trade_managed?access_token="+token;
// JSONObject mapSec = new JSONObject();
// mapSec.put("out_order_id","wx63c280fe3248a3e7");
// mapSec.put("openid","wx63c280fe3248a3e7");
// HttpResponse postSec = HttpUtil.createPost(apiurlSec)
//// .header("Authorization","Bearer 8e79d003102a4b5c80fd823c3c04347c")
//// .header("tenant-id","1")
// .body(mapSec.toJSONString())
// .execute();
// String bodySec = postSec.body();
// }
@DeleteMapping("/cancel")
@Operation(summary = "取消交易订单")
@Parameter(name = "id", description = "交易订单编号")
@ -216,4 +194,9 @@ public class AppTradeOrderController {
return success(tradeOrderUpdateService.createOrderItemCommentByMember(getLoginUserId(), createReqVO));
}
@GetMapping("/getPointOrder")
public CommonResult<List<AppPointOrderVO>> getPointOrder(){
return success(tradeOrderQueryService.getPointOrder(getLoginUserId()));
}
}

View File

@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.trade.controller.app.order.vo;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class AppPointOrderVO {
private String status; //状态
private String label;
private LocalDateTime createTime; //时间
private Integer payPrice; //支付金额
private Integer usePoint; //兑换积分
private String name; //产品名称
private String picUrl; //图片
private Integer count; //兑换数量
}

View File

@ -62,9 +62,9 @@ public class AppTradeOrderSettlementReqVO {
@Schema(description = "砍价记录编号", example = "123")
private Long bargainRecordId;
// ========== 优惠活动相关字段 ==========
@Schema(description = "满减送活动编号", example = "123")
private Long rewardId;
// ========== 积分商城活动相关字段 ==========
@Schema(description = "积分商城活动编号", example = "123")
private Long pointActivityId;
@AssertTrue(message = "活动商品每次只能购买一种规格")
@JsonIgnore

View File

@ -1,12 +1,13 @@
package cn.iocoder.yudao.module.trade.controller.app.order.vo;
import cn.iocoder.yudao.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "用户 App - 交易订单结算信息 Response VO")
@ -19,6 +20,9 @@ public class AppTradeOrderSettlementRespVO {
@Schema(description = "购物项数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Item> items;
@Schema(description = "优惠劵数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Coupon> coupons; // 可用 + 不可用
@Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED)
private Price price;
@ -26,13 +30,17 @@ public class AppTradeOrderSettlementRespVO {
private Address address;
@Schema(description = "已使用的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer usedPoint;
private Integer usePoint;
@Schema(description = "总积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer totalPoint;
@Schema(description = "商品使用积分状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "111")
private Integer pointsStatus;
/**
* 营销活动数组
*
* 只对应 {@link TradePriceCalculateRespBO.Price#} 商品匹配的活动
*/
private List<TradePriceCalculateRespBO.Promotion> promotions;
@Schema(description = "购物项")
@Data
@ -112,7 +120,6 @@ public class AppTradeOrderSettlementRespVO {
private String mobile;
@Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "地区编号不能为空")
private Long areaId;
@Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区")
private String areaName;
@ -125,4 +132,43 @@ public class AppTradeOrderSettlementRespVO {
}
@Schema(description = "优惠劵信息")
@Data
public static class Coupon {
@Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送")
private String name;
@Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位0 - 不限制
private Integer usePrice;
@Schema(description = "固定日期 - 生效开始时间")
private LocalDateTime validStartTime;
@Schema(description = "固定日期 - 生效结束时间")
private LocalDateTime validEndTime;
@Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer discountType;
@Schema(description = "折扣百分比", example = "80") // 例如说80% 80
private Integer discountPercent;
@Schema(description = "优惠金额", example = "10")
private Integer discountPrice;
@Schema(description = "折扣上限", example = "100") // 单位仅在 discountType PERCENT 使用
private Integer discountLimitPrice;
@Schema(description = "是否可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean match;
@Schema(description = "不可用原因", example = "优惠劵已过期")
private String mismatchReason;
}
}

View File

@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.trade.controller.app.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Schema(description = "用户 App - 商品结算信息 Response VO")
@Data
public class AppTradeProductSettlementRespVO {
@Schema(description = "SPU 商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long spuId;
@Schema(description = "SKU 价格信息数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private List<Sku> skus;
@Schema(description = "满减送活动信息", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private RewardActivity rewardActivity;
@Schema(description = "SKU 价格信息")
@Data
public static class Sku implements Serializable {
@Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "优惠后价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer promotionPrice;
@Schema(description = "营销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "4")
private Integer promotionType; // 对应 PromotionTypeEnum 枚举目前只有 4 6 两种
@Schema(description = "营销编号", requiredMode = Schema.RequiredMode.REQUIRED)
private Long promotionId; // 目前只有限时折扣活动的编号
@Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime promotionEndTime;
}
@Schema(description = "满减送活动信息")
@Data
public static class RewardActivity {
@Schema(description = "满减活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "条件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer conditionType;
@Schema(description = "优惠规则的数组", requiredMode = Schema.RequiredMode.REQUIRED)
private List<RewardActivityRule> rules;
}
@Schema(description = "优惠规则")
@Data
public static class RewardActivityRule {
@Schema(description = "优惠门槛", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 1. N 单位; 2. N
private Integer limit;
@Schema(description = "优惠价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer discountPrice;
@Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean freeDelivery;
@Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private Integer point;
@Schema(description = "赠送的优惠劵编号的数组")
private Map<Long, Integer> giveCouponTemplateCounts;
}
}

View File

@ -219,7 +219,9 @@ public interface TradeOrderConvert {
.setSeckillActivityId(settlementReqVO.getSeckillActivityId())
.setBargainRecordId(settlementReqVO.getBargainRecordId())
.setCombinationActivityId(settlementReqVO.getCombinationActivityId())
.setCombinationHeadId(settlementReqVO.getCombinationHeadId());
.setCombinationHeadId(settlementReqVO.getCombinationHeadId())
.setPointActivityId(settlementReqVO.getPointActivityId());
// 商品项的构建
Map<Long, CartDO> cartMap = convertMap(cartList, CartDO::getId);
for (AppTradeOrderSettlementReqVO.Item item : settlementReqVO.getItems()) {

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.dal.dataobject.order;
import cn.iocoder.yudao.framework.common.enums.TerminalEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
@ -12,10 +13,14 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderRefundStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 交易订单 DO
@ -291,6 +296,24 @@ public class TradeOrderDO extends BaseDO {
*/
private Integer vipPrice;
/**
* 赠送的优惠劵
*
* key: 优惠劵模版编号
* value对应的优惠券数量
*
* 目的用于订单支付后赠送优惠券
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<Long, Integer> giveCouponTemplateCounts;
/**
* 赠送的优惠劵编号
*
* 目的用于后续取消或者售后订单时需要扣减赠送
*/
@TableField(typeHandler = LongListTypeHandler.class)
private List<Long> giveCouponIds;
/**
* 秒杀活动编号
*
@ -330,4 +353,11 @@ public class TradeOrderDO extends BaseDO {
*/
private Long combinationRecordId;
/**
* 积分商城活动的编号
*
* 关联 PointActivityDO id 字段
*/
private Long pointActivityId;
}

View File

@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.aftersale.AfterSaleDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.cart.CartDO;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
@ -20,6 +21,7 @@ import java.util.List;
* @author 芋道源码
*/
@TableName(value = "trade_order_item", autoResultMap = true)
@KeySequence("trade_order_item_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)

View File

@ -5,10 +5,12 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppPointOrderVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderPageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
@ -129,4 +131,11 @@ public interface TradeOrderMapper extends BaseMapperX<TradeOrderDO> {
wrapperX.eq(TradeOrderDO::getUserId, userId).eq(TradeOrderDO::getSeckillActivityId, activityId);
return selectList(wrapperX);
}
@Select(" SELECT a.status,d.label,a.create_time,a.pay_price,a.use_point,c.name,c.pic_url,b.count FROM trade_order a " +
" inner join trade_order_item b on a.id = b.order_id " +
" inner join (SELECT sort, label, value, dict_type, css_class FROM system_dict_data WHERE dict_type = 'trade_order_status') d on d.sort = a.status "+
" inner join product_spu c on b.spu_id = c.id " +
" where a.user_id = #{userId} and a.use_point > 0 ")
List<AppPointOrderVO> getPointOrder(Long userId);
}

View File

@ -40,7 +40,7 @@ public class ExpressClientFactoryImpl implements ExpressClientFactory {
}
private ExpressClient createExpressClient(ExpressClientEnum queryProviderEnum,
TradeExpressProperties tradeExpressProperties) {
TradeExpressProperties tradeExpressProperties) {
switch (queryProviderEnum) {
case NOT_PROVIDE:
return new NoProvideExpressClient();

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.trade.service.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderPageReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderSummaryRespVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppPointOrderVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderPageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
@ -163,4 +164,9 @@ public interface TradeOrderQueryService {
* @return 拼团商品数量
*/
Integer getCombinationProductCount(Long userId, Long activityId);
List<AppPointOrderVO> getPointOrder(Long userId);
}

View File

@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderPageReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderSummaryRespVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppPointOrderVO;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeOrderPageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.delivery.DeliveryExpressDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
@ -259,6 +260,12 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
return tradeOrderItemMapper.selectProductSumByOrderId(convertSet(orders, TradeOrderDO::getId));
}
@Override
public List<AppPointOrderVO> getPointOrder(Long userId) {
List<AppPointOrderVO> pointOrder = tradeOrderMapper.getPointOrder(userId);
return pointOrder;
}
/**
* 获得自身的代理对象解决 AOP 生效问题
*
@ -268,4 +275,8 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService {
return SpringUtil.getBean(getClass());
}
}

View File

@ -41,6 +41,16 @@ public interface TradeOrderUpdateService {
*/
TradeOrderDO createOrder(Long userId, AppTradeOrderCreateReqVO createReqVO);
/**
* 同步订单的支付状态
*
* 1. Quietly 表示即使同步失败也不会抛出异常
* 2. 什么时候回出现异常因为是主动同步可能和支付模块的回调通知 {@link #updateOrderPaid(Long, Long)} 存在并发冲突导致抛出异常
*
* @param id 订单编号
* @param payOrderId 支付订单编号
*/
void syncOrderPayStatusQuietly(Long id, Long payOrderId);
/**
* 更新交易订单已支付
*

View File

@ -23,6 +23,8 @@ import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.pointorder.PointOrderDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.pointorder.PointOrderMapper;
import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
@ -75,6 +77,7 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.min
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getTerminal;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_NOT_FOUND;
/**
* 交易订单Service 实现类
@ -105,6 +108,9 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Resource
private TradeMessageService tradeMessageService;
@Resource
private PointOrderMapper pointOrderMapper;
@Resource
private PayOrderApi payOrderApi;
@Resource
@ -118,13 +124,17 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
@Resource
private SocialUserApi socialUserApi;
@Resource
private ProductSpuApi productSpuApi;
// =================== Order ===================
@Override
public AppTradeOrderSettlementRespVO settlementOrder(Long userId, AppTradeOrderSettlementReqVO settlementReqVO) {
// settlementReqVO.setPointActivityId((long) 9);
// 1. 获得收货地址
MemberAddressRespDTO address = getAddress(userId, settlementReqVO.getAddressId());
if (address != null) {
@ -135,21 +145,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, settlementReqVO);
// 3. 拼接返回
AppTradeOrderSettlementRespVO convert = TradeOrderConvert.INSTANCE.convert(calculateRespBO, address);
//默认禁用积分
convert.setPointsStatus(0);
//spu数组
ArrayList<Long> list = new ArrayList<>();
for (TradePriceCalculateRespBO.OrderItem item : calculateRespBO.getItems()) {
list.add(item.getSpuId());
}
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(list);
for (ProductSpuRespDTO respDTO : spuList) {
if (respDTO.getPointsStatus() == 1){
convert.setPointsStatus(1);
}
}
return convert;
return TradeOrderConvert.INSTANCE.convert(calculateRespBO, address);
}
/**
@ -182,9 +178,10 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
TradePriceCalculateReqBO calculateReqBO = TradeOrderConvert.INSTANCE.convert(userId, settlementReqVO, cartList);
calculateReqBO.getItems().forEach(item -> Assert.isTrue(item.getSelected(), // 防御性编程保证都是选中的
"商品({}) 未设置为选中", item.getSkuId()));
return tradePriceService.calculatePrice(calculateReqBO);
return tradePriceService.calculateOrderPrice(calculateReqBO);
}
@Override
@Transactional(rollbackFor = Exception.class)
@TradeOrderLog(operateType = TradeOrderOperateTypeEnum.MEMBER_CREATE)
@ -203,11 +200,46 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
orderItems.forEach(orderItem -> orderItem.setOrderId(order.getId()));
tradeOrderItemMapper.insertBatch(orderItems);
//添加进积分订单记录
if (order.getUsePoint() > 0){
//查询spu_id
TradeOrderItemDO tradeOrderItemDO = tradeOrderItemMapper.selectOne("order_id", order.getId());
PointOrderDO pointOrderDO = new PointOrderDO();
pointOrderDO.setOrderNumber(order.getNo());
pointOrderDO.setUserId(order.getUserId());
pointOrderDO.setProductId(tradeOrderItemDO.getSpuId());
pointOrderDO.setIntegral(order.getUsePoint());
pointOrderDO.setOrderStatus(order.getStatus());
pointOrderDO.setOrderTime(order.getCreateTime());
pointOrderMapper.insert(pointOrderDO);
}
// 4. 订单创建后的逻辑
afterCreateTradeOrder(order, orderItems, createReqVO);
return order;
}
@Override
public void syncOrderPayStatusQuietly(Long id, Long payOrderId) {
PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId);
if (payOrder == null) {
return;
}
if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) {
return;
}
try {
getSelf().updateOrderPaid(id, payOrderId);
} catch (Throwable e) {
log.warn("[syncOrderPayStatusQuietly][id({}) payOrderId({}) 同步支付状态失败]", id, payOrderId, e);
}
}
private TradeOrderDO buildTradeOrder(Long userId, AppTradeOrderCreateReqVO createReqVO,
TradePriceCalculateRespBO calculateRespBO) {
TradeOrderDO order = TradeOrderConvert.INSTANCE.convert(userId, createReqVO, calculateRespBO);
@ -217,6 +249,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus());
order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum));
order.setUserIp(getClientIP()).setTerminal(getTerminal());
// 使用 + 赠送优惠券
order.setGiveCouponTemplateCounts(calculateRespBO.getGiveCouponTemplateCounts());
// 支付 + 退款信息
order.setAdjustPrice(0).setPayStatus(false);
order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0);
@ -260,7 +294,10 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
}
// 3. 生成预支付
createPayOrder(order, orderItems);
// 特殊情况积分兑换时可能支付金额为零
if (order.getPayPrice() > 0) {
createPayOrder(order, orderItems);
}
// 4. 插入订单日志
TradeOrderLogUtils.setOrderInfo(order.getId(), null, order.getStatus());
@ -305,6 +342,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
TradeOrderLogUtils.setUserInfo(order.getUserId(), UserTypeEnum.MEMBER.getValue());
}
/**
* 校验交易订单满足被支付的条件
* <p>

View File

@ -78,12 +78,10 @@ public class TradeMemberPointOrderHandler implements TradeOrderHandler {
@Override
public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
// 扣减回滚积分订单赠送
reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM,
orderItem.getId());
// 增加回滚积分订单抵扣
addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM,
orderItem.getId());
addPoint(order.getUserId(), orderItem.getUsePoint(), MemberPointBizTypeEnum.ORDER_USE_CANCEL_ITEM, orderItem.getId());
// 扣减回滚积分订单赠送
reducePoint(order.getUserId(), orderItem.getGivePoint(), MemberPointBizTypeEnum.ORDER_GIVE_CANCEL_ITEM, orderItem.getId());
// 扣减回滚用户经验
AfterSaleDO afterSale = afterSaleService.getAfterSale(orderItem.getAfterSaleId());

View File

@ -0,0 +1,71 @@
package cn.iocoder.yudao.module.trade.service.order.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
/**
* 积分商城活动订单的 {@link TradeOrderHandler} 实现类
*
* @author HUIHUI
*/
@Component
public class TradePointOrderHandler implements TradeOrderHandler {
@Resource
private PointActivityApi pointActivityApi;
@Override
public void beforeOrderCreate(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
if (!TradeOrderTypeEnum.isPoint(order.getType())) {
return;
}
// 明确校验一下
Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
// 扣减积分商城活动的库存
pointActivityApi.updatePointStockDecr(order.getPointActivityId(),
orderItems.get(0).getSkuId(), orderItems.get(0).getCount());
// 如果支付金额为 0则直接设置为已支付
if (Objects.equals(order.getPayPrice(), 0)) {
order.setPayStatus(true).setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus());
}
}
@Override
public void afterCancelOrder(TradeOrderDO order, List<TradeOrderItemDO> orderItems) {
if (!TradeOrderTypeEnum.isPoint(order.getType())) {
return;
}
// 明确校验一下
Assert.isTrue(orderItems.size() == 1, "积分商城活动兑换商品兑换时,只允许选择一个商品");
// 售后的订单项已经在 afterCancelOrderItem 回滚库存所以这里不需要重复回滚
orderItems = filterOrderItemListByNoneAfterSale(orderItems);
if (CollUtil.isEmpty(orderItems)) {
return;
}
afterCancelOrderItem(order, orderItems.get(0));
}
@Override
public void afterCancelOrderItem(TradeOrderDO order, TradeOrderItemDO orderItem) {
if (!TradeOrderTypeEnum.isPoint(order.getType())) {
return;
}
// 恢复积分商城活动的库存
pointActivityApi.updatePointStockIncr(order.getPointActivityId(),
orderItem.getSkuId(), orderItem.getCount());
}
}

View File

@ -18,6 +18,16 @@ public interface TradePriceService {
* @param calculateReqDTO 计算信息
* @return 计算结果
*/
TradePriceCalculateRespBO calculatePrice(@Valid TradePriceCalculateReqBO calculateReqDTO);
// TradePriceCalculateRespBO calculatePrice(@Valid TradePriceCalculateReqBO calculateReqDTO);
/**
* 订单价格计算
*
* @param calculateReqDTO 计算信息
* @return 计算结果
*/
TradePriceCalculateRespBO calculateOrderPrice(@Valid TradePriceCalculateReqBO calculateReqDTO);
}

View File

@ -1,11 +1,21 @@
package cn.iocoder.yudao.module.trade.service.price;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi;
import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityMatchRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.controller.app.order.vo.AppTradeProductSettlementRespVO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradeDiscountActivityPriceCalculator;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculator;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper;
import lombok.extern.slf4j.Slf4j;
@ -17,8 +27,7 @@ import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_PAY_PRICE_ILLEGAL;
@ -37,12 +46,19 @@ public class TradePriceServiceImpl implements TradePriceService {
private ProductSkuApi productSkuApi;
@Resource
private ProductSpuApi productSpuApi;
@Resource
private DiscountActivityApi discountActivityApi;
@Resource
private RewardActivityApi rewardActivityApi;
@Resource
private List<TradePriceCalculator> priceCalculators;
@Resource
private TradeDiscountActivityPriceCalculator discountActivityPriceCalculator;
@Override
public TradePriceCalculateRespBO calculatePrice(TradePriceCalculateReqBO calculateReqBO) {
public TradePriceCalculateRespBO calculateOrderPrice(TradePriceCalculateReqBO calculateReqBO) {
// 1.1 获得商品 SKU 数组
List<ProductSkuRespDTO> skuList = checkSkuList(calculateReqBO);
// 1.2 获得商品 SPU 数组
@ -51,12 +67,10 @@ public class TradePriceServiceImpl implements TradePriceService {
// 2.1 计算价格
TradePriceCalculateRespBO calculateRespBO = TradePriceCalculatorHelper
.buildCalculateResp(calculateReqBO, spuList, skuList);
// priceCalculators.forEach(calculator -> calculator.calculate(calculateReqBO, calculateRespBO));
for (TradePriceCalculator calculator : priceCalculators) {
calculator.calculate(calculateReqBO, calculateRespBO);
}
priceCalculators.forEach(calculator -> calculator.calculate(calculateReqBO, calculateRespBO));
// 2.2 如果最终支付金额小于等于 0则抛出业务异常
if (calculateRespBO.getPrice().getPayPrice() <= 0) {
if (calculateReqBO.getPointActivityId() == null // 积分订单允许支付金额为 0
&& calculateRespBO.getPrice().getPayPrice() <= 0) {
log.error("[calculatePrice][价格计算不正确,请求 calculateReqDTO({}),结果 priceCalculate({})]",
calculateReqBO, calculateRespBO);
throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);
@ -84,8 +98,9 @@ public class TradePriceServiceImpl implements TradePriceService {
}
private List<ProductSpuRespDTO> checkSpuList(List<ProductSkuRespDTO> skuList) {
// 获得商品 SPU 数组
return productSpuApi.validateSpuList(convertSet(skuList, ProductSkuRespDTO::getSpuId));
}
}

View File

@ -84,11 +84,11 @@ public class TradePriceCalculateReqBO {
*/
private Long bargainRecordId;
// ========== 满减送活动相关字段 ==========
// ========== 积分商城活动相关字段 ==========
/**
* 满减送活动编号
* 积分商城活动编号
*/
private Long rewardId;
private Long pointActivityId;
/**
* 商品 SKU

View File

@ -5,7 +5,9 @@ import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 价格计算 Response BO
@ -44,17 +46,25 @@ public class TradePriceCalculateRespBO {
private List<Promotion> promotions;
/**
* 优惠劵编号
* 使用的优惠劵编号
*/
private Long couponId;
/**
* 用户的优惠劵列表可用 + 不可用
*/
private List<Coupon> coupons;
/**
* 会员剩余积分
*/
private Integer totalPoint;
/**
* 使用的积分
*/
private Integer usePoint;
/**
* 使用的积分
* 赠送的积分
*/
private Integer givePoint;
@ -63,6 +73,21 @@ public class TradePriceCalculateRespBO {
*/
private Long bargainActivityId;
/**
* 是否包邮
*/
private Boolean freeDelivery;
/**
* 赠送的优惠劵
*
* key: 优惠劵模版编号
* value对应的优惠券数量
*
* 目的用于订单支付后赠送优惠券
*/
private Map<Long, Integer> giveCouponTemplateCounts;
/**
* 订单价格
*/
@ -209,8 +234,19 @@ public class TradePriceCalculateRespBO {
*/
private Long categoryId;
// ========== 物流相关字段 =========
/**
* 运费模板 Id
* 配送方式数组
*
* 对应 DeliveryTypeEnum 枚举
*/
private List<Integer> deliveryTypes;
/**
* 物流配置模板编号
*
* 对应 TradeDeliveryExpressTemplateDO id 编号
*/
private Long deliveryTemplateId;
@ -230,7 +266,7 @@ public class TradePriceCalculateRespBO {
private List<ProductPropertyValueDetailRespDTO> properties;
/**
* 使用的积分
* 赠送的积分
*/
private Integer givePoint;
@ -308,4 +344,62 @@ public class TradePriceCalculateRespBO {
}
/**
* 优惠劵信息
*/
@Data
public static class Coupon {
/**
* 优惠劵编号
*/
private Long id;
/**
* 优惠劵名
*/
private String name;
/**
* 是否设置满多少金额可用单位
*/
private Integer usePrice;
/**
* 生效开始时间
*/
private LocalDateTime validStartTime;
/**
* 生效结束时间
*/
private LocalDateTime validEndTime;
/**
* 优惠类型
*/
private Integer discountType;
/**
* 折扣百分比
*/
private Integer discountPercent;
/**
* 优惠金额单位
*/
private Integer discountPrice;
/**
* 折扣上限单位
*/
private Integer discountLimitPrice;
/**
* 是否匹配
*/
private Boolean match;
/**
* 不匹配的原因
*/
private String mismatchReason;
}
}

View File

@ -50,5 +50,4 @@ public class TradeCombinationActivityPriceCalculator implements TradePriceCalcul
TradePriceCalculatorHelper.recountPayPrice(orderItem);
TradePriceCalculatorHelper.recountAllPrice(result);
}
}

View File

@ -4,12 +4,15 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.promotion.api.coupon.CouponApi;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponRespDTO;
import cn.iocoder.yudao.module.promotion.api.coupon.dto.CouponValidReqDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
@ -21,9 +24,11 @@ import java.util.List;
import java.util.function.Predicate;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_CAN_NOT_USE;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER;
/**
@ -40,33 +45,37 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 1.1 校验优惠劵
// 只有普通订单才允许使用优惠劵
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
if (param.getCouponId() != null) {
throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
}
return;
}
// 1.1 加载用户的优惠劵列表
List<CouponRespDTO> coupons = couponApi.getCouponListByUserId(param.getUserId(), CouponStatusEnum.UNUSED.getStatus());
coupons.removeIf(coupon -> LocalDateTimeUtils.beforeNow(coupon.getValidEndTime()));
// 1.2 计算优惠劵的使用条件
result.setCoupons(calculateCoupons(coupons, result));
// 2. 校验优惠劵是否可用
if (param.getCouponId() == null) {
return;
}
CouponRespDTO coupon = couponApi.validateCoupon(new CouponValidReqDTO()
.setId(param.getCouponId()).setUserId(param.getUserId()));
Assert.notNull(coupon, "校验通过的优惠劵({}),不能为空", param.getCouponId());
// 1.2 只有普通订单才允许使用优惠劵
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
throw exception(PRICE_CALCULATE_COUPON_NOT_MATCH_NORMAL_ORDER);
TradePriceCalculateRespBO.Coupon couponBO = CollUtil.findOne(result.getCoupons(), item -> item.getId().equals(param.getCouponId()));
CouponRespDTO coupon = CollUtil.findOne(coupons, item -> item.getId().equals(param.getCouponId()));
if (couponBO == null || coupon == null) {
throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, "优惠劵不存在");
}
// 2.1 获得匹配的商品 SKU 数组
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
if (CollUtil.isEmpty(orderItems)) {
throw exception(COUPON_NO_MATCH_SPU);
}
// 2.2 计算是否满足优惠劵的使用金额
Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
if (totalPayPrice < coupon.getUsePrice()) {
throw exception(COUPON_NO_MATCH_MIN_PRICE);
if (Boolean.FALSE.equals(couponBO.getMatch())) {
throw exception(PRICE_CALCULATE_COUPON_CAN_NOT_USE, couponBO.getMismatchReason());
}
// 3.1 计算可以优惠的金额
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
Assert.isTrue(couponPrice < totalPayPrice,
"优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice);
// 3.2 计算分摊的优惠金额
List<Integer> divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice);
@ -74,7 +83,7 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
result.setCouponId(param.getCouponId());
// 4.2 记录优惠明细
TradePriceCalculatorHelper.addPromotion(result, orderItems,
param.getCouponId(), coupon.getName(), PromotionTypeEnum.COUPON.getType(),
param.getCouponId(), couponBO.getName(), PromotionTypeEnum.COUPON.getType(),
StrUtil.format("优惠劵:省 {} 元", TradePriceCalculatorHelper.formatPrice(couponPrice)),
divideCouponPrices);
// 4.3 更新 SKU 优惠金额
@ -86,6 +95,43 @@ public class TradeCouponPriceCalculator implements TradePriceCalculator {
TradePriceCalculatorHelper.recountAllPrice(result);
}
/**
* 计算用户的优惠劵列表可用 + 不可用
*
* @param coupons 优惠劵
* @param result 计算结果
* @return 优惠劵列表
*/
private List<TradePriceCalculateRespBO.Coupon> calculateCoupons(List<CouponRespDTO> coupons,
TradePriceCalculateRespBO result) {
return convertList(coupons, coupon -> {
TradePriceCalculateRespBO.Coupon matchCoupon = BeanUtils.toBean(coupon, TradePriceCalculateRespBO.Coupon.class);
// 1.1 优惠劵未到使用时间
if (LocalDateTimeUtils.afterNow(coupon.getValidStartTime())) {
return matchCoupon.setMatch(false).setMismatchReason("优惠劵未到使用时间");
}
// 1.2 优惠劵没有匹配的商品
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterMatchCouponOrderItems(result, coupon);
if (CollUtil.isEmpty(orderItems)) {
return matchCoupon.setMatch(false).setMismatchReason("优惠劵没有匹配的商品");
}
// 1.3 %1$,.2f 元可用优惠劵
Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
if (totalPayPrice < coupon.getUsePrice()) {
return matchCoupon.setMatch(false)
.setMismatchReason(String.format("差 %1$,.2f 元可用优惠劵", (coupon.getUsePrice() - totalPayPrice) / 100D));
}
// 1.4 优惠金额超过订单金额
Integer couponPrice = getCouponPrice(coupon, totalPayPrice);
if (couponPrice >= totalPayPrice) {
return matchCoupon.setMatch(false).setMismatchReason("优惠金额超过订单金额");
}
// 2. 满足条件
return matchCoupon.setMatch(true);
});
}
private Integer getCouponPrice(CouponRespDTO coupon, Integer totalPayPrice) {
if (PromotionDiscountTypeEnum.PRICE.getType().equals(coupon.getDiscountType())) { // 减价
return coupon.getDiscountPrice();

View File

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.member.api.address.MemberAddressApi;
import cn.iocoder.yudao.module.member.api.address.dto.MemberAddressRespDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
@ -29,6 +30,7 @@ import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL;
/**
* 运费的 {@link TradePriceCalculator} 实现类
@ -55,6 +57,11 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
if (param.getDeliveryType() == null) {
return;
}
// 校验是不是存在商品不能门店自提或者不能快递发货的情况就是说配送方式不匹配哈
if (CollectionUtils.anyMatch(result.getItems(), item -> !item.getDeliveryTypes().contains(param.getDeliveryType()))) {
throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TYPE_ILLEGAL);
}
if (DeliveryTypeEnum.PICK_UP.getType().equals(param.getDeliveryType())) {
calculateByPickUp(param);
} else if (DeliveryTypeEnum.EXPRESS.getType().equals(param.getDeliveryType())) {
@ -89,7 +96,12 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
return;
}
// 情况二快递模版
// 情况二活动包邮
if (Boolean.TRUE.equals(result.getFreeDelivery())) {
return;
}
// 情况三快递模版
// 2.1 过滤出已选中的商品 SKU
List<OrderItem> selectedItem = filterList(result.getItems(), OrderItem::getSelected);
Set<Long> deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId);
@ -111,9 +123,9 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
*/
private boolean isGlobalExpressFree(TradePriceCalculateRespBO result) {
TradeConfigDO config = tradeConfigService.getTradeConfig();
return config != null
&& Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮
&& result.getPrice().getPayPrice() >= config.getDeliveryExpressFreePrice(); // 满足包邮的价格
return config == null
|| Boolean.TRUE.equals(config.getDeliveryExpressFreeEnabled()) // 开启包邮
|| result.getPrice().getPayPrice() >= config.getDeliveryExpressFreePrice(); // 满足包邮的价格
}
private void calculateDeliveryPrice(List<OrderItem> selectedSkus,
@ -123,7 +135,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
Map<Long, List<OrderItem>> template2ItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId);
// 依次计算快递运费
for (Map.Entry<Long, List<OrderItem>> entry : template2ItemMap.entrySet()) {
Long templateId = entry.getKey();
Long templateId = entry.getKey();
List<OrderItem> orderItems = entry.getValue();
DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId);
if (templateBO == null) {
@ -143,8 +155,8 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
/**
* 按配送方式来计算运费
*
* @param orderItems SKU 商品项目
* @param chargeMode 配送计费方式
* @param orderItems SKU 商品项目
* @param chargeMode 配送计费方式
* @param templateCharge 快递运费配置
*/
private void calculateExpressFeeByChargeMode(List<OrderItem> orderItems, Integer chargeMode,

View File

@ -3,6 +3,10 @@ package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.api.discount.DiscountActivityApi;
import cn.iocoder.yudao.module.promotion.api.discount.dto.DiscountProductRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionDiscountTypeEnum;
@ -19,6 +23,7 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.calculateRatePrice;
import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
/**
@ -33,44 +38,115 @@ public class TradeDiscountActivityPriceCalculator implements TradePriceCalculato
@Resource
private DiscountActivityApi discountActivityApi;
@Resource
private MemberUserApi memberUserApi;
@Resource
private MemberLevelApi memberLevelApi;
/**
* 计算会员 VIP 的优惠价格
*
* @param level 会员等级
* @param orderItem 交易项
* @return 优惠价格
*/
public Integer calculateVipPrice(MemberLevelRespDTO level,
TradePriceCalculateRespBO.OrderItem orderItem) {
if (level == null || level.getDiscountPercent() == null) {
return 0;
}
Integer newPrice = calculateRatePrice(orderItem.getPayPrice(), level.getDiscountPercent().doubleValue());
return orderItem.getPayPrice() - newPrice;
}
/**
* 计算优惠活动的价格
*
* @param discount 优惠活动
* @param orderItem 交易项
* @return 优惠价格
*/
public Integer calculateActivityPrice(DiscountProductRespDTO discount,
TradePriceCalculateRespBO.OrderItem orderItem) {
if (discount == null) {
return 0;
}
Integer newPrice = orderItem.getPayPrice();
if (PromotionDiscountTypeEnum.PRICE.getType().equals(discount.getDiscountType())) { // 减价
newPrice -= discount.getDiscountPrice() * orderItem.getCount();
} else if (PromotionDiscountTypeEnum.PERCENT.getType().equals(discount.getDiscountType())) { // 打折
newPrice = calculateRatePrice(orderItem.getPayPrice(), discount.getDiscountPercent() / 100.0);
} else {
throw new IllegalArgumentException(String.format("优惠活动的商品(%s) 的优惠类型不正确", discount));
}
return orderItem.getPayPrice() - newPrice;
}
/**
* 获得用户的等级
*
* @param userId 用户编号
* @return 用户等级
*/
public MemberLevelRespDTO getMemberLevel(Long userId) {
MemberUserRespDTO user = memberUserApi.getUser(userId);
if (user == null || user.getLevelId() == null || user.getLevelId() <= 0) {
return null;
}
return memberLevelApi.getMemberLevel(user.getLevelId());
}
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 0. 只有普通订单才计算该优惠
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
return;
}
// 获得 SKU 对应的限时折扣活动
// 1.1 获得 SKU 对应的限时折扣活动
List<DiscountProductRespDTO> discountProducts = discountActivityApi.getMatchDiscountProductList(
convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSkuId));
if (CollUtil.isEmpty(discountProducts)) {
return;
}
Map<Long, DiscountProductRespDTO> discountProductMap = convertMap(discountProducts, DiscountProductRespDTO::getSkuId);
// 1.2 获得会员等级
MemberLevelRespDTO level = getMemberLevel(param.getUserId());
// 处理每个 SKU 的限时折扣
// 2. 计算每个 SKU 的优惠金额
result.getItems().forEach(orderItem -> {
// 1. 获取该 SKU 的优惠信息
DiscountProductRespDTO discountProduct = discountProductMap.get(orderItem.getSkuId());
if (discountProduct == null) {
if (!orderItem.getSelected()) {
return;
}
// 2.1 计算限时折扣的优惠金额
DiscountProductRespDTO discountProduct = discountProductMap.get(orderItem.getSkuId());
Integer discountPrice = calculateActivityPrice(discountProduct, orderItem);
// 2.2 计算 VIP 优惠金额
Integer vipPrice = calculateVipPrice(level, orderItem);
if (discountPrice <= 0 && vipPrice <= 0) {
return;
}
// 2. 计算优惠金额
Integer newPayPrice = calculatePayPrice(discountProduct, orderItem);
Integer newDiscountPrice = orderItem.getPayPrice() - newPayPrice;
// 3.1 记录优惠明细
if (orderItem.getSelected()) {
// 注意只有在选中的情况下才会记录到优惠明细否则仅仅是更新 SKU 优惠金额用于展示
// 3. 选择优惠金额多的
if (discountPrice > vipPrice) {
TradePriceCalculatorHelper.addPromotion(result, orderItem,
discountProduct.getActivityId(), discountProduct.getActivityName(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType(),
StrUtil.format("限时折扣:省 {} 元", formatPrice(newDiscountPrice)),
newDiscountPrice);
StrUtil.format("限时折扣:省 {} 元", formatPrice(discountPrice)),
discountPrice);
// 更新 SKU 优惠金额
orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice);
} else {
assert level != null;
TradePriceCalculatorHelper.addPromotion(result, orderItem,
level.getId(), level.getName(), PromotionTypeEnum.MEMBER_LEVEL.getType(),
String.format("会员等级折扣:省 %s 元", formatPrice(vipPrice)),
vipPrice);
// 更新 SKU 的优惠金额
orderItem.setVipPrice(vipPrice);
}
// 3.2 更新 SKU 优惠金额
orderItem.setDiscountPrice(orderItem.getDiscountPrice() + newDiscountPrice);
// 4. 分摊优惠
TradePriceCalculatorHelper.recountPayPrice(orderItem);
TradePriceCalculatorHelper.recountAllPrice(result);
});
TradePriceCalculatorHelper.recountAllPrice(result);
}
private Integer calculatePayPrice(DiscountProductRespDTO discountProduct,

View File

@ -1,88 +1,88 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
/**
* 会员 VIP 折扣的 {@link TradePriceCalculator} 实现类
*
* @author 芋道源码
*/
@Component
@Order(TradePriceCalculator.ORDER_MEMBER_LEVEL)
public class TradeMemberLevelPriceCalculator implements TradePriceCalculator {
@Resource
private MemberLevelApi memberLevelApi;
@Resource
private MemberUserApi memberUserApi;
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 0. 只有普通订单才计算该优惠
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
return;
}
// 1. 获得用户的会员等级
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
if (user.getLevelId() == null || user.getLevelId() <= 0) {
return;
}
MemberLevelRespDTO level = memberLevelApi.getMemberLevel(user.getLevelId());
if (level == null || level.getDiscountPercent() == null) {
return;
}
// 2. 计算每个 SKU 的优惠金额
result.getItems().forEach(orderItem -> {
// 2.1 计算优惠金额
Integer vipPrice = calculateVipPrice(orderItem.getPayPrice(), level.getDiscountPercent());
if (vipPrice <= 0) {
return;
}
// 2.2 记录优惠明细
if (orderItem.getSelected()) {
// 注意只有在选中的情况下才会记录到优惠明细否则仅仅是更新 SKU 优惠金额用于展示
TradePriceCalculatorHelper.addPromotion(result, orderItem,
level.getId(), level.getName(), PromotionTypeEnum.MEMBER_LEVEL.getType(),
String.format("会员等级折扣:省 %s 元", formatPrice(vipPrice)),
vipPrice);
}
// 2.3 更新 SKU 的优惠金额
orderItem.setVipPrice(vipPrice);
TradePriceCalculatorHelper.recountPayPrice(orderItem);
});
TradePriceCalculatorHelper.recountAllPrice(result);
}
/**
* 计算会员 VIP 优惠价格
*
* @param price 原价
* @param discountPercent 折扣
* @return 优惠价格
*/
public Integer calculateVipPrice(Integer price, Integer discountPercent) {
if (discountPercent == null) {
return 0;
}
Integer newPrice = price * discountPercent / 100;
return price - newPrice;
}
}
//package cn.iocoder.yudao.module.trade.service.price.calculator;
//
//import cn.hutool.core.util.ObjectUtil;
//import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
//import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
//import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
//import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
//import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
//import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
//import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
//import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
//import org.springframework.core.annotation.Order;
//import org.springframework.stereotype.Component;
//
//import javax.annotation.Resource;
//
//import static cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice;
//
///**
// * 会员 VIP 折扣的 {@link TradePriceCalculator} 实现类
// *
// * @author 芋道源码
// */
//@Component
//@Order(TradePriceCalculator.ORDER_MEMBER_LEVEL)
//public class TradeMemberLevelPriceCalculator implements TradePriceCalculator {
//
// @Resource
// private MemberLevelApi memberLevelApi;
// @Resource
// private MemberUserApi memberUserApi;
//
// @Override
// public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// // 0. 只有普通订单才计算该优惠
// if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.NORMAL.getType())) {
// return;
// }
// // 1. 获得用户的会员等级
// MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
// if (user.getLevelId() == null || user.getLevelId() <= 0) {
// return;
// }
// MemberLevelRespDTO level = memberLevelApi.getMemberLevel(user.getLevelId());
// if (level == null || level.getDiscountPercent() == null) {
// return;
// }
//
// // 2. 计算每个 SKU 的优惠金额
// result.getItems().forEach(orderItem -> {
// // 2.1 计算优惠金额
// Integer vipPrice = calculateVipPrice(orderItem.getPayPrice(), level.getDiscountPercent());
// if (vipPrice <= 0) {
// return;
// }
//
// // 2.2 记录优惠明细
// if (orderItem.getSelected()) {
// // 注意只有在选中的情况下才会记录到优惠明细否则仅仅是更新 SKU 优惠金额用于展示
// TradePriceCalculatorHelper.addPromotion(result, orderItem,
// level.getId(), level.getName(), PromotionTypeEnum.MEMBER_LEVEL.getType(),
// String.format("会员等级折扣:省 %s 元", formatPrice(vipPrice)),
// vipPrice);
// }
//
// // 2.3 更新 SKU 的优惠金额
// orderItem.setVipPrice(vipPrice);
// TradePriceCalculatorHelper.recountPayPrice(orderItem);
// });
// TradePriceCalculatorHelper.recountAllPrice(result);
// }
//
// /**
// * 计算会员 VIP 优惠价格
// *
// * @param price 原价
// * @param discountPercent 折扣
// * @return 优惠价格
// */
// public Integer calculateVipPrice(Integer price, Integer discountPercent) {
// if (discountPercent == null) {
// return 0;
// }
// Integer newPrice = price * discountPercent / 100;
// return price - newPrice;
// }
//
//}

View File

@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.api.point.PointActivityApi;
import cn.iocoder.yudao.module.promotion.api.point.dto.PointValidateJoinRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT;
/**
* 积分商城的 {@link TradePriceCalculator} 实现类
*
* @author owen
*/
@Component
@Order(TradePriceCalculator.ORDER_POINT_ACTIVITY)
@Slf4j
public class TradePointActivityPriceCalculator implements TradePriceCalculator {
@Resource
private PointActivityApi pointActivityApi;
@Resource
private MemberUserApi memberUserApi;
@Resource
private TradeOrderQueryService tradeOrderQueryService;
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 1.1 判断订单类型是否为积分商城活动
if (ObjectUtil.notEqual(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
return;
}
// 1.2 初始化积分
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
result.setTotalPoint(user.getPoint()).setUsePoint(0);
// 1.3 校验用户积分余额
if (user.getPoint() == null || user.getPoint() <= 0) {
return;
}
Assert.isTrue(param.getItems().size() == 1, "积分商城兑换商品时,只允许选择一个商品");
// 2. 校验是否可以参与积分商城活动
TradePriceCalculateRespBO.OrderItem orderItem = result.getItems().get(0);
PointValidateJoinRespDTO activity = validateJoinSeckill(
param.getUserId(), param.getPointActivityId(),
orderItem.getSkuId(), orderItem.getCount());
// 3.1 记录优惠明细
int discountPrice = orderItem.getPayPrice(); // 情况一单使用积分兑换
Assert.isTrue(activity.getPoint() >= 1, "积分商城商品兑换积分必须大于 1");
result.setUsePoint(activity.getPoint() * orderItem.getCount());
orderItem.setUsePoint(activity.getPoint() * orderItem.getCount());
if (activity.getPrice() != null && activity.getPrice() > 0) { // 情况二积分 + 金额
discountPrice = orderItem.getPayPrice() - activity.getPrice() * orderItem.getCount();
}
// 3.2 记录优惠明细
TradePriceCalculatorHelper.addPromotion(result, orderItem,
param.getPointActivityId(), "积分商城活动", PromotionTypeEnum.POINT.getType(),
StrUtil.format("积分商城活动:省 {} 元", TradePriceCalculatorHelper.formatPrice(discountPrice)),
discountPrice);
// 3.3 更新 SKU 优惠金额
orderItem.setDiscountPrice(orderItem.getDiscountPrice() + discountPrice);
TradePriceCalculatorHelper.recountPayPrice(orderItem);
TradePriceCalculatorHelper.recountAllPrice(result);
}
private PointValidateJoinRespDTO validateJoinSeckill(Long userId, Long activityId, Long skuId, Integer count) {
// 1. 校验是否可以参与积分商城活动
PointValidateJoinRespDTO pointValidateJoinRespDTO = pointActivityApi.validateJoinPointActivity(activityId, skuId, count);
// 2. 校验总限购数量目前只有 trade 有具体下单的数据需要交给 trade 价格计算使用
int activityProductCount = tradeOrderQueryService.getSeckillProductCount(userId, activityId);
if (activityProductCount + count > pointValidateJoinRespDTO.getCount()) {
throw exception(PRICE_CALCULATE_POINT_TOTAL_LIMIT_COUNT);
}
return pointValidateJoinRespDTO;
}
}

View File

@ -1,15 +1,14 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.member.api.config.MemberConfigApi;
import cn.iocoder.yudao.module.member.api.config.dto.MemberConfigRespDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import lombok.extern.slf4j.Slf4j;
@ -17,8 +16,6 @@ import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -39,58 +36,33 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
private MemberConfigApi memberConfigApi;
@Resource
private MemberUserApi memberUserApi;
@Resource
private ProductSpuApi productSpuApi;
@Override
public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) {
// 默认使用积分为 0
result.setUsePoint(0);
// 判断订单类型是否不为积分商城活动
if (ObjectUtil.equal(result.getType(), TradeOrderTypeEnum.POINT.getType())) {
return;
}
// 0. 初始化积分
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
result.setTotalPoint(user.getPoint()).setUsePoint(0);
// 1.1 校验是否使用积分
if (!BooleanUtil.isTrue(param.getPointStatus())) {
result.setUsePoint(0);
return;
}
// 1.2 校验积分抵扣是否开启
MemberConfigRespDTO config = memberConfigApi.getConfig();
//全局
if (!isDeductPointEnable(config)) {
return;
}
//部分商品
ArrayList<Long> list = new ArrayList<>();
for (TradePriceCalculateRespBO.OrderItem item : result.getItems()) {
list.add(item.getSpuId());
}
List<ProductSpuRespDTO> spuList = productSpuApi.getSpuList(list);
int status = 0;
int usePointMax = 0;
for (ProductSpuRespDTO respDTO : spuList) {
usePointMax += respDTO.getUsePointsMax();
if (respDTO.getPointsStatus() == 1){
status = 1;
}
}
if (status == 0){
return;
}
// 1.3 校验用户积分余额
MemberUserRespDTO user = memberUserApi.getUser(param.getUserId());
if (user.getPoint() == null || user.getPoint() <= 0) {
return;
}
int calculatePointPricePar = 0;
if (user.getPoint() >= usePointMax){
calculatePointPricePar = usePointMax;
}else {
calculatePointPricePar = user.getPoint();
}
// 2.1 计算积分优惠金额
int pointPrice = calculatePointPrice(config, calculatePointPricePar, result);
int pointPrice = calculatePointPrice(config, user.getPoint(), result);
// 2.2 计算分摊的积分抵扣金额
List<TradePriceCalculateRespBO.OrderItem> orderItems = filterList(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSelected);
List<Integer> dividePointPrices = TradePriceCalculatorHelper.dividePrice(orderItems, pointPrice);
@ -125,18 +97,18 @@ public class TradePointUsePriceCalculator implements TradePriceCalculator {
// TODO @疯狂这里应该是抵扣到只剩下 0.01
// 积分优惠金额
int pointPrice = usePoint * config.getPointTradeDeductUnitPrice();
// if (result.getPrice().getPayPrice() <= pointPrice) {
// // 禁止 0 元购
// throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);
// }
// // 允许0 元购!!!用户积分比较多时积分可以抵扣的金额要大于支付金额这时需要根据支付金额反推使用多少积分
if (result.getPrice().getPayPrice() <= pointPrice) {
pointPrice = result.getPrice().getPayPrice() - 1;
// 反推需要扣除的积分
usePoint = NumberUtil.toBigDecimal(pointPrice)
.divide(NumberUtil.toBigDecimal(config.getPointTradeDeductUnitPrice()), 0, RoundingMode.HALF_UP)
.intValue();
// 禁止 0 元购
throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL);
}
// // 允许0 元购!!!用户积分比较多时积分可以抵扣的金额要大于支付金额这时需要根据支付金额反推使用多少积分
// if (result.getPrice().getPayPrice() < pointPrice) {
// pointPrice = result.getPrice().getPayPrice();
// // 反推需要扣除的积分
// usePoint = NumberUtil.toBigDecimal(pointPrice)
// .divide(NumberUtil.toBigDecimal(config.getPointTradeDeductUnitPrice()), 0, RoundingMode.HALF_UP)
// .intValue();
// }
// 记录使用的积分
result.setUsePoint(usePoint);
return pointPrice;

View File

@ -18,6 +18,7 @@ public interface TradePriceCalculator {
int ORDER_SECKILL_ACTIVITY = 8;
int ORDER_BARGAIN_ACTIVITY = 8;
int ORDER_COMBINATION_ACTIVITY = 8;
int ORDER_POINT_ACTIVITY = 8;
int ORDER_DISCOUNT_ACTIVITY = 10;
int ORDER_REWARD_ACTIVITY = 20;

View File

@ -1,18 +1,17 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.promotion.api.reward.RewardActivityApi;
import cn.iocoder.yudao.module.promotion.api.reward.dto.RewardActivityDTO;
import cn.iocoder.yudao.module.trade.dal.dataobject.order.TradeOrderItemDO;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -28,15 +27,12 @@ import static java.util.Collections.singletonList;
* @author 芋道源码
*/
public class TradePriceCalculatorHelper {
@Resource
private static RewardActivityApi rewardActivityApi;
public static TradePriceCalculateRespBO buildCalculateResp(TradePriceCalculateReqBO param,
List<ProductSpuRespDTO> spuList, List<ProductSkuRespDTO> skuList) {
// 创建 PriceCalculateRespDTO 对象
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO();
result.setType(getOrderType(param));
result.setPromotions(new ArrayList<>());
result.setType(getOrderType(param)).setPromotions(new ArrayList<>()).setGiveCouponTemplateCounts(new LinkedHashMap<>());
// 创建它的 OrderItem 属性
result.setItems(new ArrayList<>(param.getItems().size()));
@ -64,9 +60,9 @@ public class TradePriceCalculatorHelper {
.setWeight(sku.getWeight()).setVolume(sku.getVolume());
// spu 信息
orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
.setDeliveryTemplateId(spu.getDeliveryTemplateId())
.setDeliveryTypes(spu.getDeliveryTypes()).setDeliveryTemplateId(spu.getDeliveryTemplateId())
.setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
if (orderItem.getPicUrl() == null) {
if (StrUtil.isBlank(orderItem.getPicUrl())) {
orderItem.setPicUrl(spu.getPicUrl());
}
});
@ -75,30 +71,6 @@ public class TradePriceCalculatorHelper {
result.setPrice(new TradePriceCalculateRespBO.Price());
recountAllPrice(result);
recountAllGivePoint(result);
// param.setRewardId(1L);
//满减送活动优惠价格计算
// if (param.getRewardId() != null){
// RewardActivityDTO activityDTO = rewardActivityApi.getRewardActivityById(param.getRewardId());
// TradePriceCalculateRespBO.Price price = result.getPrice();
//// List<TradePriceCalculateRespBO.OrderItem> items = result.getItems();
// int limitPrice = 0;
// int discountPrice = 0;
// Boolean deliveryPrice = null;
// for (RewardActivityDTO.Rule rule : activityDTO.getRules()) {
// if (price.getTotalPrice() >= rule.getLimit() && rule.getLimit() > limitPrice){
// limitPrice = rule.getLimit();
// discountPrice = rule.getDiscountPrice();
// deliveryPrice = rule.getFreeDelivery();
// }
// }
// price.setDiscountPrice(price.getDiscountPrice() + discountPrice);
// if (deliveryPrice != null && deliveryPrice){
// price.setDeliveryPrice(0);
// }
// price.setPayPrice(price.getPayPrice() - discountPrice);
// }
return result;
}
@ -118,6 +90,9 @@ public class TradePriceCalculatorHelper {
if (param.getBargainRecordId() != null) {
return TradeOrderTypeEnum.BARGAIN.getType();
}
if (param.getPointActivityId() != null) {
return TradeOrderTypeEnum.POINT.getType();
}
return TradeOrderTypeEnum.NORMAL.getType();
}
@ -269,7 +244,7 @@ public class TradePriceCalculatorHelper {
*
* {@link #dividePrice(List, Integer)} 逻辑一致只是传入的是 TradeOrderItemDO 对象
*
* @param items 订单项
* @param items 订单项
* @param price 订单支付金额
* @return 分摊金额数组和传入的 orderItems 一一对应
*/

View File

@ -44,9 +44,10 @@ public class TradeRewardActivityPriceCalculator implements TradePriceCalculator
if (CollUtil.isEmpty(rewardActivities)) {
return;
}
// 处理每个满减送活动
rewardActivities.forEach(rewardActivity -> calculate(param, result, rewardActivity));
// 处理最新的满减送活动
if (!rewardActivities.isEmpty()) {
calculate(param, result, rewardActivities.get(0));
}
}
private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result,

View File

@ -17,6 +17,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_SECKILL_TOTAL_LIMIT_COUNT;
// TODO huihui单测需要补充
/**
* 秒杀活动的 {@link TradePriceCalculator} 实现类
*

View File

@ -1,137 +1,137 @@
package cn.iocoder.yudao.module.trade.service.price;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.product.api.property.dto.ProductPropertyValueDetailRespDTO;
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculator;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
/**
* {@link TradePriceServiceImpl} 的单元测试
*
* @author 芋道源码
*/
@Disabled // TODO 芋艿后续 fix 补充的单测
public class TradePriceServiceImplTest extends BaseMockitoUnitTest {
@InjectMocks
private TradePriceServiceImpl tradePriceService;
@Mock
private ProductSkuApi productSkuApi;
@Mock
private ProductSpuApi productSpuApi;
@Mock
private List<TradePriceCalculator> priceCalculators;
@Test
public void testCalculatePrice() {
// 准备参数
TradePriceCalculateReqBO calculateReqBO = new TradePriceCalculateReqBO()
.setUserId(10L)
.setCouponId(20L).setAddressId(30L)
.setItems(Arrays.asList(
new TradePriceCalculateReqBO.Item().setSkuId(100L).setCount(1).setSelected(true),
new TradePriceCalculateReqBO.Item().setSkuId(200L).setCount(3).setSelected(true),
new TradePriceCalculateReqBO.Item().setSkuId(300L).setCount(6).setCartId(233L).setSelected(false)
));
// mock 方法
List<ProductSkuRespDTO> skuList = Arrays.asList(
new ProductSkuRespDTO().setId(100L).setStock(500).setPrice(1000).setPicUrl("https://t.cn/1.png").setSpuId(1001L)
.setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色")
.setValueId(2L).setValueName("红色"))),
new ProductSkuRespDTO().setId(200L).setStock(400).setPrice(2000).setPicUrl("https://t.cn/2.png").setSpuId(1001L)
.setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色")
.setValueId(3L).setValueName("黄色"))),
new ProductSkuRespDTO().setId(300L).setStock(600).setPrice(3000).setPicUrl("https://t.cn/3.png").setSpuId(1001L)
.setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色")
.setValueId(4L).setValueName("黑色")))
);
when(productSkuApi.getSkuList(Mockito.eq(asSet(100L, 200L, 300L)))).thenReturn(skuList);
when(productSpuApi.getSpuList(Mockito.eq(asSet(1001L))))
.thenReturn(singletonList(new ProductSpuRespDTO().setId(1001L).setName("小菜").setCategoryId(666L)
.setStatus(ProductSpuStatusEnum.ENABLE.getStatus())));
// 调用
TradePriceCalculateRespBO calculateRespBO = tradePriceService.calculatePrice(calculateReqBO);
// 断言
assertEquals(TradeOrderTypeEnum.NORMAL.getType(), calculateRespBO.getType());
assertEquals(0, calculateRespBO.getPromotions().size());
assertNull(calculateRespBO.getCouponId());
// 断言订单价格
assertEquals(7000, calculateRespBO.getPrice().getTotalPrice());
assertEquals(0, calculateRespBO.getPrice().getDiscountPrice());
assertEquals(0, calculateRespBO.getPrice().getDeliveryPrice());
assertEquals(0, calculateRespBO.getPrice().getCouponPrice());
assertEquals(0, calculateRespBO.getPrice().getPointPrice());
assertEquals(7000, calculateRespBO.getPrice().getPayPrice());
// 断言SKU 1
assertEquals(1001L, calculateRespBO.getItems().get(0).getSpuId());
assertEquals(100L, calculateRespBO.getItems().get(0).getSkuId());
assertEquals(1, calculateRespBO.getItems().get(0).getCount());
assertNull(calculateRespBO.getItems().get(0).getCartId());
assertTrue(calculateRespBO.getItems().get(0).getSelected());
assertEquals(1000, calculateRespBO.getItems().get(0).getPrice());
assertEquals(0, calculateRespBO.getItems().get(0).getDiscountPrice());
assertEquals(0, calculateRespBO.getItems().get(0).getDeliveryPrice());
assertEquals(0, calculateRespBO.getItems().get(0).getCouponPrice());
assertEquals(0, calculateRespBO.getItems().get(0).getPointPrice());
assertEquals(1000, calculateRespBO.getItems().get(0).getPayPrice());
assertEquals("小菜", calculateRespBO.getItems().get(0).getSpuName());
assertEquals("https://t.cn/1.png", calculateRespBO.getItems().get(0).getPicUrl());
assertEquals(666L, calculateRespBO.getItems().get(0).getCategoryId());
assertEquals(skuList.get(0).getProperties(), calculateRespBO.getItems().get(0).getProperties());
// 断言SKU 2
assertEquals(1001L, calculateRespBO.getItems().get(1).getSpuId());
assertEquals(200L, calculateRespBO.getItems().get(1).getSkuId());
assertEquals(3, calculateRespBO.getItems().get(1).getCount());
assertNull(calculateRespBO.getItems().get(1).getCartId());
assertTrue(calculateRespBO.getItems().get(1).getSelected());
assertEquals(2000, calculateRespBO.getItems().get(1).getPrice());
assertEquals(0, calculateRespBO.getItems().get(1).getDiscountPrice());
assertEquals(0, calculateRespBO.getItems().get(1).getDeliveryPrice());
assertEquals(0, calculateRespBO.getItems().get(1).getCouponPrice());
assertEquals(0, calculateRespBO.getItems().get(1).getPointPrice());
assertEquals(6000, calculateRespBO.getItems().get(1).getPayPrice());
assertEquals("小菜", calculateRespBO.getItems().get(1).getSpuName());
assertEquals("https://t.cn/2.png", calculateRespBO.getItems().get(1).getPicUrl());
assertEquals(666L, calculateRespBO.getItems().get(1).getCategoryId());
assertEquals(skuList.get(1).getProperties(), calculateRespBO.getItems().get(1).getProperties());
// 断言SKU 3
assertEquals(1001L, calculateRespBO.getItems().get(2).getSpuId());
assertEquals(300L, calculateRespBO.getItems().get(2).getSkuId());
assertEquals(6, calculateRespBO.getItems().get(2).getCount());
assertEquals(233L, calculateRespBO.getItems().get(2).getCartId());
assertFalse(calculateRespBO.getItems().get(2).getSelected());
assertEquals(3000, calculateRespBO.getItems().get(2).getPrice());
assertEquals(0, calculateRespBO.getItems().get(2).getDiscountPrice());
assertEquals(0, calculateRespBO.getItems().get(2).getDeliveryPrice());
assertEquals(0, calculateRespBO.getItems().get(2).getCouponPrice());
assertEquals(0, calculateRespBO.getItems().get(2).getPointPrice());
assertEquals(18000, calculateRespBO.getItems().get(2).getPayPrice());
assertEquals("小菜", calculateRespBO.getItems().get(2).getSpuName());
assertEquals("https://t.cn/3.png", calculateRespBO.getItems().get(2).getPicUrl());
assertEquals(666L, calculateRespBO.getItems().get(2).getCategoryId());
assertEquals(skuList.get(2).getProperties(), calculateRespBO.getItems().get(2).getProperties());
}
}
//package cn.iocoder.yudao.module.trade.service.price;
//
//import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
//import cn.iocoder.yudao.module.product.api.property.dto.ProductPropertyValueDetailRespDTO;
//import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
//import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
//import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
//import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
//import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
//import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
//import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
//import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
//import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculator;
//import org.junit.jupiter.api.Disabled;
//import org.junit.jupiter.api.Test;
//import org.mockito.InjectMocks;
//import org.mockito.Mock;
//import org.mockito.Mockito;
//
//import java.util.Arrays;
//import java.util.List;
//
//import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
//import static java.util.Collections.singletonList;
//import static org.junit.jupiter.api.Assertions.*;
//import static org.mockito.Mockito.when;
//
///**
// * {@link TradePriceServiceImpl} 的单元测试
// *
// * @author 芋道源码
// */
//@Disabled // TODO 芋艿后续 fix 补充的单测
//public class TradePriceServiceImplTest extends BaseMockitoUnitTest {
//
// @InjectMocks
// private TradePriceServiceImpl tradePriceService;
//
// @Mock
// private ProductSkuApi productSkuApi;
// @Mock
// private ProductSpuApi productSpuApi;
// @Mock
// private List<TradePriceCalculator> priceCalculators;
//
// @Test
// public void testCalculatePrice() {
// // 准备参数
// TradePriceCalculateReqBO calculateReqBO = new TradePriceCalculateReqBO()
// .setUserId(10L)
// .setCouponId(20L).setAddressId(30L)
// .setItems(Arrays.asList(
// new TradePriceCalculateReqBO.Item().setSkuId(100L).setCount(1).setSelected(true),
// new TradePriceCalculateReqBO.Item().setSkuId(200L).setCount(3).setSelected(true),
// new TradePriceCalculateReqBO.Item().setSkuId(300L).setCount(6).setCartId(233L).setSelected(false)
// ));
// // mock 方法
// List<ProductSkuRespDTO> skuList = Arrays.asList(
// new ProductSkuRespDTO().setId(100L).setStock(500).setPrice(1000).setPicUrl("https://t.cn/1.png").setSpuId(1001L)
// .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色")
// .setValueId(2L).setValueName("红色"))),
// new ProductSkuRespDTO().setId(200L).setStock(400).setPrice(2000).setPicUrl("https://t.cn/2.png").setSpuId(1001L)
// .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色")
// .setValueId(3L).setValueName("黄色"))),
// new ProductSkuRespDTO().setId(300L).setStock(600).setPrice(3000).setPicUrl("https://t.cn/3.png").setSpuId(1001L)
// .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色")
// .setValueId(4L).setValueName("黑色")))
// );
// when(productSkuApi.getSkuList(Mockito.eq(asSet(100L, 200L, 300L)))).thenReturn(skuList);
// when(productSpuApi.getSpuList(Mockito.eq(asSet(1001L))))
// .thenReturn(singletonList(new ProductSpuRespDTO().setId(1001L).setName("小菜").setCategoryId(666L)
// .setStatus(ProductSpuStatusEnum.ENABLE.getStatus())));
//
// // 调用
// TradePriceCalculateRespBO calculateRespBO = tradePriceService.calculatePrice(calculateReqBO);
// // 断言
// assertEquals(TradeOrderTypeEnum.NORMAL.getType(), calculateRespBO.getType());
// assertEquals(0, calculateRespBO.getPromotions().size());
// assertNull(calculateRespBO.getCouponId());
// // 断言订单价格
// assertEquals(7000, calculateRespBO.getPrice().getTotalPrice());
// assertEquals(0, calculateRespBO.getPrice().getDiscountPrice());
// assertEquals(0, calculateRespBO.getPrice().getDeliveryPrice());
// assertEquals(0, calculateRespBO.getPrice().getCouponPrice());
// assertEquals(0, calculateRespBO.getPrice().getPointPrice());
// assertEquals(7000, calculateRespBO.getPrice().getPayPrice());
// // 断言SKU 1
// assertEquals(1001L, calculateRespBO.getItems().get(0).getSpuId());
// assertEquals(100L, calculateRespBO.getItems().get(0).getSkuId());
// assertEquals(1, calculateRespBO.getItems().get(0).getCount());
// assertNull(calculateRespBO.getItems().get(0).getCartId());
// assertTrue(calculateRespBO.getItems().get(0).getSelected());
// assertEquals(1000, calculateRespBO.getItems().get(0).getPrice());
// assertEquals(0, calculateRespBO.getItems().get(0).getDiscountPrice());
// assertEquals(0, calculateRespBO.getItems().get(0).getDeliveryPrice());
// assertEquals(0, calculateRespBO.getItems().get(0).getCouponPrice());
// assertEquals(0, calculateRespBO.getItems().get(0).getPointPrice());
// assertEquals(1000, calculateRespBO.getItems().get(0).getPayPrice());
// assertEquals("小菜", calculateRespBO.getItems().get(0).getSpuName());
// assertEquals("https://t.cn/1.png", calculateRespBO.getItems().get(0).getPicUrl());
// assertEquals(666L, calculateRespBO.getItems().get(0).getCategoryId());
// assertEquals(skuList.get(0).getProperties(), calculateRespBO.getItems().get(0).getProperties());
// // 断言SKU 2
// assertEquals(1001L, calculateRespBO.getItems().get(1).getSpuId());
// assertEquals(200L, calculateRespBO.getItems().get(1).getSkuId());
// assertEquals(3, calculateRespBO.getItems().get(1).getCount());
// assertNull(calculateRespBO.getItems().get(1).getCartId());
// assertTrue(calculateRespBO.getItems().get(1).getSelected());
// assertEquals(2000, calculateRespBO.getItems().get(1).getPrice());
// assertEquals(0, calculateRespBO.getItems().get(1).getDiscountPrice());
// assertEquals(0, calculateRespBO.getItems().get(1).getDeliveryPrice());
// assertEquals(0, calculateRespBO.getItems().get(1).getCouponPrice());
// assertEquals(0, calculateRespBO.getItems().get(1).getPointPrice());
// assertEquals(6000, calculateRespBO.getItems().get(1).getPayPrice());
// assertEquals("小菜", calculateRespBO.getItems().get(1).getSpuName());
// assertEquals("https://t.cn/2.png", calculateRespBO.getItems().get(1).getPicUrl());
// assertEquals(666L, calculateRespBO.getItems().get(1).getCategoryId());
// assertEquals(skuList.get(1).getProperties(), calculateRespBO.getItems().get(1).getProperties());
// // 断言SKU 3
// assertEquals(1001L, calculateRespBO.getItems().get(2).getSpuId());
// assertEquals(300L, calculateRespBO.getItems().get(2).getSkuId());
// assertEquals(6, calculateRespBO.getItems().get(2).getCount());
// assertEquals(233L, calculateRespBO.getItems().get(2).getCartId());
// assertFalse(calculateRespBO.getItems().get(2).getSelected());
// assertEquals(3000, calculateRespBO.getItems().get(2).getPrice());
// assertEquals(0, calculateRespBO.getItems().get(2).getDiscountPrice());
// assertEquals(0, calculateRespBO.getItems().get(2).getDeliveryPrice());
// assertEquals(0, calculateRespBO.getItems().get(2).getCouponPrice());
// assertEquals(0, calculateRespBO.getItems().get(2).getPointPrice());
// assertEquals(18000, calculateRespBO.getItems().get(2).getPayPrice());
// assertEquals("小菜", calculateRespBO.getItems().get(2).getSpuName());
// assertEquals("https://t.cn/3.png", calculateRespBO.getItems().get(2).getPicUrl());
// assertEquals(666L, calculateRespBO.getItems().get(2).getCategoryId());
// assertEquals(skuList.get(2).getProperties(), calculateRespBO.getItems().get(2).getProperties());
// }
//
//}

View File

@ -1,118 +1,118 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.ArrayList;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* {@link TradeMemberLevelPriceCalculator} 的单元测试类
*
* @author 芋道源码
*/
public class TradeMemberLevelPriceCalculatorTest extends BaseMockitoUnitTest {
@InjectMocks
private TradeMemberLevelPriceCalculator memberLevelPriceCalculator;
@Mock
private MemberLevelApi memberLevelApi;
@Mock
private MemberUserApi memberUserApi;
@Test
public void testCalculate() {
// 准备参数
TradePriceCalculateReqBO param = new TradePriceCalculateReqBO()
.setUserId(1024L)
.setItems(asList(
new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配活动且已选中
new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(false) // 匹配活动但未选中
));
TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
.setType(TradeOrderTypeEnum.NORMAL.getType())
.setPrice(new TradePriceCalculateRespBO.Price())
.setPromotions(new ArrayList<>())
.setItems(asList(
new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
.setPrice(100),
new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(false)
.setPrice(50)
));
// 保证价格被初始化上
TradePriceCalculatorHelper.recountPayPrice(result.getItems());
TradePriceCalculatorHelper.recountAllPrice(result);
// mock 方法会员等级
when(memberUserApi.getUser(eq(1024L))).thenReturn(new MemberUserRespDTO().setLevelId(2048L));
when(memberLevelApi.getMemberLevel(eq(2048L))).thenReturn(
new MemberLevelRespDTO().setId(2048L).setName("VIP 会员").setDiscountPercent(60));
// 调用
memberLevelPriceCalculator.calculate(param, result);
// 断言Price 部分
TradePriceCalculateRespBO.Price price = result.getPrice();
assertEquals(price.getTotalPrice(), 200);
assertEquals(price.getDiscountPrice(), 0);
assertEquals(price.getPointPrice(), 0);
assertEquals(price.getDeliveryPrice(), 0);
assertEquals(price.getCouponPrice(), 0);
assertEquals(price.getVipPrice(), 80);
assertEquals(price.getPayPrice(), 120);
assertNull(result.getCouponId());
// 断言SKU 1
assertEquals(result.getItems().size(), 2);
TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0);
assertEquals(orderItem01.getSkuId(), 10L);
assertEquals(orderItem01.getCount(), 2);
assertEquals(orderItem01.getPrice(), 100);
assertEquals(orderItem01.getDiscountPrice(), 0);
assertEquals(orderItem01.getDeliveryPrice(), 0);
assertEquals(orderItem01.getCouponPrice(), 0);
assertEquals(orderItem01.getPointPrice(), 0);
assertEquals(orderItem01.getVipPrice(), 80);
assertEquals(orderItem01.getPayPrice(), 120);
// 断言SKU 2
TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
assertEquals(orderItem02.getSkuId(), 20L);
assertEquals(orderItem02.getCount(), 3);
assertEquals(orderItem02.getPrice(), 50);
assertEquals(orderItem02.getDiscountPrice(), 0);
assertEquals(orderItem02.getDeliveryPrice(), 0);
assertEquals(orderItem02.getCouponPrice(), 0);
assertEquals(orderItem02.getPointPrice(), 0);
assertEquals(orderItem02.getVipPrice(), 60);
assertEquals(orderItem02.getPayPrice(), 90);
// 断言Promotion 部分
assertEquals(result.getPromotions().size(), 1);
TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
assertEquals(promotion01.getId(), 2048L);
assertEquals(promotion01.getName(), "VIP 会员");
assertEquals(promotion01.getType(), PromotionTypeEnum.MEMBER_LEVEL.getType());
assertEquals(promotion01.getTotalPrice(), 200);
assertEquals(promotion01.getDiscountPrice(), 80);
assertTrue(promotion01.getMatch());
assertEquals(promotion01.getDescription(), "会员等级折扣:省 0.80 元");
TradePriceCalculateRespBO.PromotionItem promotionItem01 = promotion01.getItems().get(0);
assertEquals(promotion01.getItems().size(), 1);
assertEquals(promotionItem01.getSkuId(), 10L);
assertEquals(promotionItem01.getTotalPrice(), 200);
assertEquals(promotionItem01.getDiscountPrice(), 80);
}
}
//package cn.iocoder.yudao.module.trade.service.price.calculator;
//
//import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
//import cn.iocoder.yudao.module.member.api.level.MemberLevelApi;
//import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO;
//import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
//import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
//import cn.iocoder.yudao.module.promotion.enums.common.PromotionTypeEnum;
//import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
//import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
//import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
//import org.junit.jupiter.api.Test;
//import org.mockito.InjectMocks;
//import org.mockito.Mock;
//
//import java.util.ArrayList;
//
//import static java.util.Arrays.asList;
//import static org.junit.jupiter.api.Assertions.*;
//import static org.mockito.ArgumentMatchers.eq;
//import static org.mockito.Mockito.when;
//
///**
// * {@link TradeMemberLevelPriceCalculator} 的单元测试类
// *
// * @author 芋道源码
// */
//public class TradeMemberLevelPriceCalculatorTest extends BaseMockitoUnitTest {
//
// @InjectMocks
// private TradeMemberLevelPriceCalculator memberLevelPriceCalculator;
//
// @Mock
// private MemberLevelApi memberLevelApi;
// @Mock
// private MemberUserApi memberUserApi;
//
// @Test
// public void testCalculate() {
// // 准备参数
// TradePriceCalculateReqBO param = new TradePriceCalculateReqBO()
// .setUserId(1024L)
// .setItems(asList(
// new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配活动且已选中
// new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(false) // 匹配活动但未选中
// ));
// TradePriceCalculateRespBO result = new TradePriceCalculateRespBO()
// .setType(TradeOrderTypeEnum.NORMAL.getType())
// .setPrice(new TradePriceCalculateRespBO.Price())
// .setPromotions(new ArrayList<>())
// .setItems(asList(
// new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true)
// .setPrice(100),
// new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(false)
// .setPrice(50)
// ));
// // 保证价格被初始化上
// TradePriceCalculatorHelper.recountPayPrice(result.getItems());
// TradePriceCalculatorHelper.recountAllPrice(result);
//
// // mock 方法会员等级
// when(memberUserApi.getUser(eq(1024L))).thenReturn(new MemberUserRespDTO().setLevelId(2048L));
// when(memberLevelApi.getMemberLevel(eq(2048L))).thenReturn(
// new MemberLevelRespDTO().setId(2048L).setName("VIP 会员").setDiscountPercent(60));
//
// // 调用
// memberLevelPriceCalculator.calculate(param, result);
// // 断言Price 部分
// TradePriceCalculateRespBO.Price price = result.getPrice();
// assertEquals(price.getTotalPrice(), 200);
// assertEquals(price.getDiscountPrice(), 0);
// assertEquals(price.getPointPrice(), 0);
// assertEquals(price.getDeliveryPrice(), 0);
// assertEquals(price.getCouponPrice(), 0);
// assertEquals(price.getVipPrice(), 80);
// assertEquals(price.getPayPrice(), 120);
// assertNull(result.getCouponId());
// // 断言SKU 1
// assertEquals(result.getItems().size(), 2);
// TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0);
// assertEquals(orderItem01.getSkuId(), 10L);
// assertEquals(orderItem01.getCount(), 2);
// assertEquals(orderItem01.getPrice(), 100);
// assertEquals(orderItem01.getDiscountPrice(), 0);
// assertEquals(orderItem01.getDeliveryPrice(), 0);
// assertEquals(orderItem01.getCouponPrice(), 0);
// assertEquals(orderItem01.getPointPrice(), 0);
// assertEquals(orderItem01.getVipPrice(), 80);
// assertEquals(orderItem01.getPayPrice(), 120);
// // 断言SKU 2
// TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1);
// assertEquals(orderItem02.getSkuId(), 20L);
// assertEquals(orderItem02.getCount(), 3);
// assertEquals(orderItem02.getPrice(), 50);
// assertEquals(orderItem02.getDiscountPrice(), 0);
// assertEquals(orderItem02.getDeliveryPrice(), 0);
// assertEquals(orderItem02.getCouponPrice(), 0);
// assertEquals(orderItem02.getPointPrice(), 0);
// assertEquals(orderItem02.getVipPrice(), 60);
// assertEquals(orderItem02.getPayPrice(), 90);
// // 断言Promotion 部分
// assertEquals(result.getPromotions().size(), 1);
// TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0);
// assertEquals(promotion01.getId(), 2048L);
// assertEquals(promotion01.getName(), "VIP 会员");
// assertEquals(promotion01.getType(), PromotionTypeEnum.MEMBER_LEVEL.getType());
// assertEquals(promotion01.getTotalPrice(), 200);
// assertEquals(promotion01.getDiscountPrice(), 80);
// assertTrue(promotion01.getMatch());
// assertEquals(promotion01.getDescription(), "会员等级折扣:省 0.80 元");
// TradePriceCalculateRespBO.PromotionItem promotionItem01 = promotion01.getItems().get(0);
// assertEquals(promotion01.getItems().size(), 1);
// assertEquals(promotionItem01.getSkuId(), 10L);
// assertEquals(promotionItem01.getTotalPrice(), 200);
// assertEquals(promotionItem01.getDiscountPrice(), 80);
// }
//
//}

View File

@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.system.api.social.dto;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* 微信小程序订阅消息发送 Request DTO
*
* @author HUIHUI
*/
@Data
public class SocialWxaSubscribeMessageSendReqDTO {
/**
* 用户编号
*
* 关联 MemberUserDO id 编号
* 关联 AdminUserDO id 编号
*/
@NotNull(message = "用户编号不能为空")
private Long userId;
/**
* 用户类型
*
* 关联 {@link UserTypeEnum}
*/
@NotNull(message = "用户类型不能为空")
private Integer userType;
/**
* 消息模版标题
*/
@NotEmpty(message = "消息模版标题不能为空")
private String templateTitle;
/**
* 点击模板卡片后的跳转页面仅限本小程序内的页面
*
* 支持带参数示例 index?foo=bar 该字段不填则模板无跳转
*/
private String page;
/**
* 模板内容的参数
*/
private Map<String, String> messages;
public SocialWxaSubscribeMessageSendReqDTO addMessage(String key, String value) {
if (messages == null) {
messages = new HashMap<>();
}
messages.put(key, value);
return this;
}
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.system.api.social.dto;
import lombok.Data;
/**
* 小程序订阅消息模版 Response DTO
*
* @author HUIHUI
*/
@Data
public class SocialWxaSubscribeTemplateRespDTO {
/**
* 模版编号
*/
private String id;
/**
* 模版标题
*/
private String title;
/**
* 模版内容
*/
private String content;
/**
* 模板内容示例
*/
private String example;
/**
* 模版类型
*
* 2为一次性订阅
* 3为长期订阅
*/
private Integer type;
}

View File

@ -77,7 +77,8 @@ public interface ErrorCodeConstants {
ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1_002_011_000, "短信渠道不存在");
ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1_002_011_001, "短信渠道不处于开启状态,不允许选择");
ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1_002_011_002, "无法删除,该短信渠道还有短信模板");
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR = new ErrorCode(1_002_018_202, "获得小程序订阅消息模版失败");
ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR = new ErrorCode(1_002_018_203, "发送小程序订阅消息失败");
// ========== 短信模板 1-002-012-000 ==========
ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_012_000, "短信模板不存在");
ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1_002_012_001, "已经存在编码为【{}】的短信模板");