(no commit message provided)

Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
dxc
2026-02-09 11:29:37 +08:00
parent 49453d47f6
commit 89a29f0b65
6 changed files with 717 additions and 2 deletions

View File

@ -0,0 +1,123 @@
from flask import request, jsonify, current_app
from . import inbound_bp
from app.schemas.stock_schema import stock_service_schema
from app.services.inbound.service_service import ServiceService
from app.utils.decorators import token_required, role_required
@inbound_bp.route('/service', methods=['GET'])
@token_required
def get_service_list():
"""获取服务权益列表"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
keyword = request.args.get('keyword', None)
start_date = request.args.get('start_date', None)
end_date = request.args.get('end_date', None)
provider_name = request.args.get('provider_name', None)
try:
result = ServiceService.get_service_list(
page=page,
per_page=per_page,
keyword=keyword,
start_date=start_date,
end_date=end_date,
provider_name=provider_name
)
return jsonify({
'code': 200,
'msg': 'success',
'data': result
})
except Exception as e:
current_app.logger.error(f'获取服务列表失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@inbound_bp.route('/service', methods=['POST'])
@token_required
@role_required('admin,manager')
def create_service():
"""创建服务权益"""
data = request.get_json()
if not data:
return jsonify({'code': 400, 'msg': '请求数据为空'}), 400
errors = stock_service_schema.validate(data)
if errors:
return jsonify({'code': 400, 'msg': '数据校验失败', 'errors': errors}), 400
try:
service = ServiceService.create_service(data)
return jsonify({
'code': 201,
'msg': '创建成功',
'data': stock_service_schema.dump(service)
}), 201
except ValueError as e:
return jsonify({'code': 400, 'msg': str(e)}), 400
except Exception as e:
current_app.logger.error(f'创建服务权益失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@inbound_bp.route('/service/<int:service_id>', methods=['GET'])
@token_required
def get_service(service_id):
"""获取单个服务权益详情"""
try:
service = ServiceService.get_service(service_id)
return jsonify({
'code': 200,
'msg': 'success',
'data': stock_service_schema.dump(service)
})
except ValueError as e:
return jsonify({'code': 404, 'msg': str(e)}), 404
except Exception as e:
current_app.logger.error(f'获取服务权益详情失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@inbound_bp.route('/service/<int:service_id>', methods=['PUT'])
@token_required
@role_required('admin,manager')
def update_service(service_id):
"""更新服务权益"""
data = request.get_json()
if not data:
return jsonify({'code': 400, 'msg': '请求数据为空'}), 400
# 部分字段不允许更新,可在此过滤
allowed_fields = {'sale_price', 'provider_name', 'description'}
filtered_data = {k: v for k, v in data.items() if k in allowed_fields}
if not filtered_data:
return jsonify({'code': 400, 'msg': '无有效更新字段'}), 400
try:
service = ServiceService.update_service(service_id, filtered_data)
return jsonify({
'code': 200,
'msg': '更新成功',
'data': stock_service_schema.dump(service)
})
except ValueError as e:
return jsonify({'code': 404, 'msg': str(e)}), 404
except Exception as e:
current_app.logger.error(f'更新服务权益失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@inbound_bp.route('/service/<int:service_id>', methods=['DELETE'])
@token_required
@role_required('admin,manager')
def delete_service(service_id):
"""删除服务权益"""
try:
ServiceService.delete_service(service_id)
return jsonify({
'code': 200,
'msg': '删除成功'
})
except ValueError as e:
return jsonify({'code': 404, 'msg': str(e)}), 404
except Exception as e:
current_app.logger.error(f'删除服务权益失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500

View File

@ -0,0 +1,46 @@
from app import db
from datetime import datetime
class StockService(db.Model):
"""
服务权益库存表
对应数据库表: stock_service
"""
__tablename__ = 'stock_service'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# 关联基础物料信息
base_id = db.Column(db.Integer, db.ForeignKey('material_base.id'), nullable=False)
# 系统生成的SKU格式 SRV-YYYYMMDD-XXXX
sku = db.Column(db.String(64), unique=True, nullable=False)
# 售价
sale_price = db.Column(db.Numeric(10, 2), nullable=False)
# 服务商名称
provider_name = db.Column(db.String(255), nullable=False, default='')
# 服务详情/简介
description = db.Column(db.Text, default='')
# 创建时间与更新时间
created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, nullable=False)
# 软删除标志
is_deleted = db.Column(db.Boolean, default=False, nullable=False)
# 关系(可选)
material_base = db.relationship('MaterialBase', backref='service_stocks', lazy='joined')
def to_dict(self):
"""转为字典,用于 API 响应"""
return {
'id': self.id,
'base_id': self.base_id,
'sku': self.sku,
'sale_price': float(self.sale_price) if self.sale_price is not None else 0,
'provider_name': self.provider_name,
'description': self.description,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None,
'material_name': self.material_base.name if self.material_base else None,
'spec_model': self.material_base.spec_model if self.material_base else None,
'unit': self.material_base.unit if self.material_base else None,
}

View File

@ -38,5 +38,28 @@ class StockBuySchema(Schema):
# 这里暂时不强制抛出错误,交给 Service 层处理 "SKU不存在且无名字" 的情况 # 这里暂时不强制抛出错误,交给 Service 层处理 "SKU不存在且无名字" 的情况
class StockServiceSchema(Schema):
# 只用于输出的字段
id = fields.Int(dump_only=True)
sku = fields.Str(dump_only=True)
created_at = fields.DateTime(format='%Y-%m-%d %H:%M:%S', dump_only=True)
updated_at = fields.DateTime(format='%Y-%m-%d %H:%M:%S', dump_only=True)
material_name = fields.Str(dump_only=True)
spec_model = fields.Str(dump_only=True)
unit = fields.Str(dump_only=True)
# 输入字段
base_id = fields.Int(required=True, error_messages={"required": "必须选择基础物料"})
sale_price = fields.Float(required=True, validate=validate.Range(min=0, error="售价不能为负数"))
provider_name = fields.Str(required=True, error_messages={"required": "服务商名称不能为空"})
description = fields.Str(missing='')
@validates_schema
def validate_base_id(self, data, **kwargs):
# 可以在这里添加对 base_id 是否存在的检查,但更建议在 Service 层进行
pass
# 实例化 Schema # 实例化 Schema
stock_buy_schema = StockBuySchema() stock_buy_schema = StockBuySchema()
stock_service_schema = StockServiceSchema()

View File

@ -0,0 +1,126 @@
from app import db
from app.models.inbound.service import StockService
from app.models.material_base import MaterialBase
from datetime import datetime, timedelta
import re
class ServiceService:
"""服务权益库存业务逻辑"""
SKU_PREFIX = 'SRV'
SKU_DATE_FORMAT = '%Y%m%d'
SKU_SUFFIX_LEN = 4
@classmethod
def _generate_sku(cls):
"""生成唯一SKU格式 SRV-YYYYMMDD-XXXX"""
today_str = datetime.now().strftime(cls.SKU_DATE_FORMAT)
prefix = f'{cls.SKU_PREFIX}-{today_str}-'
# 查找今天已有的最大后缀
max_sku = db.session.query(db.func.max(StockService.sku)).filter(
StockService.sku.like(f'{prefix}%')
).scalar()
if not max_sku:
suffix_num = 1
else:
# 提取后缀数字
suffix_part = max_sku.replace(prefix, '')
match = re.match(r'^(\d+)', suffix_part)
suffix_num = int(match.group(1)) if match else 0
suffix_num += 1
# 格式化为4位数字左侧补零
suffix = str(suffix_num).zfill(cls.SKU_SUFFIX_LEN)
return f'{prefix}{suffix}'
@classmethod
def create_service(cls, data):
"""创建服务权益记录"""
# 检查基础物料是否存在
base = MaterialBase.query.get(data.get('base_id'))
if not base:
raise ValueError('基础物料不存在')
# 生成SKU
sku = cls._generate_sku()
service = StockService(
base_id=data['base_id'],
sku=sku,
sale_price=data['sale_price'],
provider_name=data['provider_name'],
description=data.get('description', '')
)
db.session.add(service)
db.session.commit()
return service
@classmethod
def get_service(cls, service_id):
"""获取单个服务权益"""
service = StockService.query.filter_by(id=service_id, is_deleted=False).first()
if not service:
raise ValueError('服务权益记录不存在')
return service
@classmethod
def update_service(cls, service_id, data):
"""更新服务权益记录"""
service = cls.get_service(service_id)
# 不允许修改 base_id 和 sku业务上不允许变更基础物料
if 'sale_price' in data:
service.sale_price = data['sale_price']
if 'provider_name' in data:
service.provider_name = data['provider_name']
if 'description' in data:
service.description = data.get('description', '')
service.updated_at = datetime.now()
db.session.commit()
return service
@classmethod
def delete_service(cls, service_id):
"""软删除服务权益"""
service = cls.get_service(service_id)
service.is_deleted = True
service.updated_at = datetime.now()
db.session.commit()
return True
@classmethod
def get_service_list(cls, page=1, per_page=20, keyword=None,
start_date=None, end_date=None, provider_name=None):
"""分页查询服务权益列表"""
query = StockService.query.filter_by(is_deleted=False)
# 关键词搜索:可搜索 SKU 或 关联物料名称
if keyword:
# 子查询查找物料名称匹配的 base_id
subquery = MaterialBase.query.filter(
MaterialBase.name.ilike(f'%{keyword}%')
).subquery()
query = query.filter(
db.or_(
StockService.sku.ilike(f'%{keyword}%'),
StockService.base_id.in_([row.id for row in db.session.query(subquery.c.id)])
)
)
if start_date:
start = datetime.strptime(start_date, '%Y-%m-%d')
query = query.filter(StockService.created_at >= start)
if end_date:
end = datetime.strptime(end_date, '%Y-%m-%d')
# 包含当天
end = end + timedelta(days=1) - timedelta(seconds=1)
query = query.filter(StockService.created_at <= end)
if provider_name:
query = query.filter(StockService.provider_name.ilike(f'%{provider_name}%'))
# 总数
total = query.count()
# 分页
items = query.order_by(StockService.created_at.desc())\
.offset((page - 1) * per_page)\
.limit(per_page).all()
return {
'items': [item.to_dict() for item in items],
'total': total,
'page': page,
'per_page': per_page
}

View File

@ -0,0 +1,91 @@
import request from '@/utils/request'
export interface ServiceItem {
id: number
base_id: number
sku: string
sale_price: number
provider_name: string
description: string
created_at: string
updated_at: string
material_name?: string
spec_model?: string
unit?: string
}
export interface ServiceListResponse {
code: number
msg: string
data: {
items: ServiceItem[]
total: number
page: number
per_page: number
}
}
export interface ServiceQueryParams {
page?: number
per_page?: number
keyword?: string
start_date?: string
end_date?: string
provider_name?: string
}
export interface ServiceCreateRequest {
base_id: number
sale_price: number
provider_name: string
description?: string
}
export interface ServiceUpdateRequest {
sale_price?: number
provider_name?: string
description?: string
}
// 获取服务权益列表
export function getServiceList(params: ServiceQueryParams) {
return request<ServiceListResponse>({
url: '/v1/inbound/service',
method: 'get',
params
})
}
// 创建服务权益
export function createService(data: ServiceCreateRequest) {
return request({
url: '/v1/inbound/service',
method: 'post',
data
})
}
// 获取服务权益详情
export function getServiceDetail(id: number) {
return request<ServiceListResponse>({
url: `/v1/inbound/service/${id}`,
method: 'get'
})
}
// 更新服务权益
export function updateService(id: number, data: ServiceUpdateRequest) {
return request({
url: `/v1/inbound/service/${id}`,
method: 'put',
data
})
}
// 删除服务权益
export function deleteService(id: number) {
return request({
url: `/v1/inbound/service/${id}`,
method: 'delete'
})
}

View File

@ -1 +1,307 @@
<template><div style="padding:20px;"><h2>服务权益管理</h2></div></template> <template>
<div style="padding: 20px;">
<h2 style="margin-bottom: 20px;">服务权益管理</h2>
<div class="header-toolbar">
<el-form :inline="true" @submit.prevent>
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="SKU/物料名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px;"
/>
</el-form-item>
<el-form-item label="服务商">
<el-input
v-model="searchForm.provider_name"
placeholder="服务商名称"
clearable
@keyup.enter="handleSearch"
style="width: 200px;"
/>
</el-form-item>
<el-form-item label="开始日期">
<el-date-picker
v-model="searchForm.start_date"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker
v-model="searchForm.end_date"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button type="success" @click="handleAdd">新增服务</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="tableData" border stripe style="width: 100%;" v-loading="loading">
<el-table-column prop="sku" label="SKU" width="200" />
<el-table-column prop="material_name" label="物料名称" />
<el-table-column prop="provider_name" label="服务商" width="150" />
<el-table-column prop="sale_price" label="售价" width="120">
<template #default="{row}">{{ row.sale_price.toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="description" label="简介" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{row}">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 20px; text-align: center;">
<el-pagination
v-model:current-page="page"
v-model:page-size="perPage"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
@close="resetDialog"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="基础物料ID" prop="base_id">
<el-input-number
v-model="form.base_id"
placeholder="请输入基础物料ID"
:controls="false"
style="width: 100%;"
/>
<div style="font-size:12px;color:#999;">需要先创建基础物料</div>
</el-form-item>
<el-form-item label="售价" prop="sale_price">
<el-input-number
v-model="form.sale_price"
placeholder="请输入售价"
:controls="false"
:precision="2"
:min="0"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="服务商" prop="provider_name">
<el-input
v-model="form.provider_name"
placeholder="请输入服务商名称"
/>
</el-form-item>
<el-form-item label="简介" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入服务简介"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleDialogConfirm">确认</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getServiceList,
createService,
updateService,
deleteService,
type ServiceItem,
type ServiceQueryParams,
type ServiceCreateRequest
} from '@/api/inbound/service'
// 表格数据
const tableData = ref<ServiceItem[]>([])
const loading = ref(false)
const page = ref(1)
const perPage = ref(20)
const total = ref(0)
const searchForm = reactive({
keyword: '',
provider_name: '',
start_date: '',
end_date: ''
})
// 加载列表
const loadData = async () => {
loading.value = true
try {
const params: ServiceQueryParams = {
page: page.value,
per_page: perPage.value,
keyword: searchForm.keyword || undefined,
provider_name: searchForm.provider_name || undefined,
start_date: searchForm.start_date || undefined,
end_date: searchForm.end_date || undefined
}
const res = await getServiceList(params)
if (res.code === 200) {
tableData.value = res.data.items
total.value = res.data.total
} else {
ElMessage.error(res.msg || '加载失败')
}
} catch (error) {
ElMessage.error('网络错误')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
page.value = 1
loadData()
}
const resetSearch = () => {
Object.assign(searchForm, {
keyword: '',
provider_name: '',
start_date: '',
end_date: ''
})
page.value = 1
loadData()
}
const handleSizeChange = (val: number) => {
perPage.value = val
loadData()
}
const handlePageChange = (val: number) => {
page.value = val
loadData()
}
// 弹窗相关
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formRef = ref<FormInstance>()
const form = reactive({
id: 0,
base_id: 0,
sale_price: 0,
provider_name: '',
description: ''
})
const rules = reactive<FormRules>({
base_id: [
{ required: true, message: '请选择基础物料', trigger: 'blur' },
{ type: 'number', min: 1, message: '物料ID必须大于0', trigger: 'blur' }
],
sale_price: [
{ required: true, message: '请输入售价', trigger: 'blur' },
{ type: 'number', min: 0, message: '售价不能为负数', trigger: 'blur' }
],
provider_name: [
{ required: true, message: '请输入服务商名称', trigger: 'blur' }
]
})
const handleAdd = () => {
dialogTitle.value = '新增服务'
Object.assign(form, {
id: 0,
base_id: 0,
sale_price: 0,
provider_name: '',
description: ''
})
dialogVisible.value = true
}
const handleEdit = (row: ServiceItem) => {
dialogTitle.value = '编辑服务'
Object.assign(form, {
id: row.id,
base_id: row.base_id,
sale_price: row.sale_price,
provider_name: row.provider_name,
description: row.description
})
dialogVisible.value = true
}
const handleDialogConfirm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
try {
const reqData: ServiceCreateRequest = {
base_id: form.base_id,
sale_price: form.sale_price,
provider_name: form.provider_name,
description: form.description
}
if (form.id === 0) {
await createService(reqData)
ElMessage.success('创建成功')
} else {
await updateService(form.id, reqData)
ElMessage.success('更新成功')
}
dialogVisible.value = false
loadData()
} catch (error: any) {
ElMessage.error(error?.response?.data?.msg || '操作失败')
}
})
}
const resetDialog = () => {
formRef.value?.clearValidate()
}
const handleDelete = (row: ServiceItem) => {
ElMessageBox.confirm(`确定删除服务权益 "${row.sku}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteService(row.id)
ElMessage.success('删除成功')
loadData()
} catch (error: any) {
ElMessage.error(error?.response?.data?.msg || '删除失败')
}
}).catch(() => {})
}
onMounted(() => {
loadData()
})
</script>