Compare commits
8 Commits
ea63cd9931
...
102c75079d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
102c75079d | ||
|
|
72bc6c9af2 | ||
|
|
f3124e96cc | ||
|
|
7f19a37591 | ||
|
|
6ed205dcb8 | ||
|
|
16ca9c02ba | ||
|
|
1b0cdde780 | ||
|
|
cbcd54b70c |
13
frontend/src/utils/replaceAllKeys.js
Normal file
13
frontend/src/utils/replaceAllKeys.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// 用 values 的键值对整体替换 target 的内容:先清空 target 已有的全部键,再赋值。
|
||||||
|
//
|
||||||
|
// 不能直接用 Object.assign(target, values):assign 只覆盖 target 上**已存在**的键,
|
||||||
|
// 不会删除多余的键。表单对象上残留的 id / organization_id 这类字段就是这么漏进来的 ——
|
||||||
|
// values(新建用的空表单)里没有 id,assign 便原样保留了上一次编辑并进来的 id,
|
||||||
|
// 于是新增请求体里带上了别人的 id。这里先 delete 再赋值,保证结果只含 values 的键。
|
||||||
|
//
|
||||||
|
// target 通常是 Vue 的 reactive 对象,delete 与赋值都会触发 Proxy 的拦截,
|
||||||
|
// 视图能正常同步;同样也适用于普通对象。
|
||||||
|
export function replaceAllKeys(target, values) {
|
||||||
|
Object.keys(target).forEach(key => { delete target[key] })
|
||||||
|
Object.assign(target, values)
|
||||||
|
}
|
||||||
45
frontend/src/utils/replaceAllKeys.test.js
Normal file
45
frontend/src/utils/replaceAllKeys.test.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import { replaceAllKeys } from './replaceAllKeys'
|
||||||
|
|
||||||
|
describe('replaceAllKeys', () => {
|
||||||
|
test('删掉 values 里没有的键,不留残留字段', () => {
|
||||||
|
const form = { id: 1, code: 'A', name: '旧名称' }
|
||||||
|
|
||||||
|
replaceAllKeys(form, { code: '', name: '' })
|
||||||
|
|
||||||
|
expect(form).toEqual({ code: '', name: '' })
|
||||||
|
expect(Object.keys(form)).not.toContain('id')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('这一点正是 Object.assign 做不到的', () => {
|
||||||
|
const assigned = { id: 1, code: 'A' }
|
||||||
|
Object.assign(assigned, { code: 'B' })
|
||||||
|
|
||||||
|
expect(assigned.id).toBe(1)
|
||||||
|
|
||||||
|
const replaced = { id: 1, code: 'A' }
|
||||||
|
replaceAllKeys(replaced, { code: 'B' })
|
||||||
|
|
||||||
|
expect(replaced.id).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('覆盖同名的键并写入新增的键', () => {
|
||||||
|
const form = { code: 'A', name: '旧名称' }
|
||||||
|
|
||||||
|
replaceAllKeys(form, { code: 'A09', name: '新增类', parent_id: null })
|
||||||
|
|
||||||
|
expect(form).toEqual({ code: 'A09', name: '新增类', parent_id: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('用于 reactive 对象时,键集合与取值都同步更新', () => {
|
||||||
|
const form = reactive({ id: 7, code: 'A' })
|
||||||
|
|
||||||
|
replaceAllKeys(form, { code: 'B', status: 'active' })
|
||||||
|
|
||||||
|
expect(Object.keys(form)).toEqual(['code', 'status'])
|
||||||
|
expect(form.id).toBeUndefined()
|
||||||
|
expect(form.code).toBe('B')
|
||||||
|
expect(form.status).toBe('active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -106,6 +106,7 @@
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { codeRulesApi, materialCategoriesApi, unitsApi } from '../../api/masterData'
|
import { codeRulesApi, materialCategoriesApi, unitsApi } from '../../api/masterData'
|
||||||
|
import { replaceAllKeys } from '../../utils/replaceAllKeys'
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'categories', label: '物料分类', api: materialCategoriesApi },
|
{ key: 'categories', label: '物料分类', api: materialCategoriesApi },
|
||||||
|
|
@ -152,7 +153,9 @@ async function ensureCategories() {
|
||||||
|
|
||||||
async function openCreate() {
|
async function openCreate() {
|
||||||
editingId.value = null
|
editingId.value = null
|
||||||
Object.assign(form, defaultForm())
|
// 用 replaceAllKeys 而不是 Object.assign:openEdit 会把 row 的全部字段并进 form,
|
||||||
|
// 其中 id / organization_id 在新增时必须清掉,assign 做不到。
|
||||||
|
replaceAllKeys(form, defaultForm())
|
||||||
if (activeTab.value === 'rules') await ensureCategories()
|
if (activeTab.value === 'rules') await ensureCategories()
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +169,7 @@ async function openEdit(row) {
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
// :rules 在本项目不生效,校验一律手写。编码规则的必填项与其他 tab 不同
|
// :rules 在本项目不生效,校验一律手写。编码规则的必填项与其他 tab 不同
|
||||||
// (分类 + 前缀,见 SaveCodeRule),所以按 tab 分支。
|
// (分类 + 前缀,见 prepareCodeRule),所以按 tab 分支。
|
||||||
if (activeTab.value === 'rules') {
|
if (activeTab.value === 'rules') {
|
||||||
if (!form.category_id) { ElMessage.warning('请选择物料分类'); return }
|
if (!form.category_id) { ElMessage.warning('请选择物料分类'); return }
|
||||||
if (!form.prefix.trim()) { ElMessage.warning('请填写编码前缀'); return }
|
if (!form.prefix.trim()) { ElMessage.warning('请填写编码前缀'); return }
|
||||||
|
|
|
||||||
|
|
@ -123,3 +123,71 @@ describe('伙伴表单必填校验', () => {
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('伙伴列表的合作状态显示', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
partnersApi.list.mockReset().mockResolvedValue({ data: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('合作状态列显示中文,不把英文枚举值直接给用户', async () => {
|
||||||
|
partnersApi.list.mockResolvedValue({
|
||||||
|
data: [{ id: 1, partner_code: 'SUP-001', legal_name: '某某钢材有限公司', supplier_status: 'active' }]
|
||||||
|
})
|
||||||
|
const wrapper = mountPartner()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('启用')
|
||||||
|
expect(wrapper.text()).not.toContain('active')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('未收录的状态原样显示,不伪装成已知状态', async () => {
|
||||||
|
partnersApi.list.mockResolvedValue({
|
||||||
|
data: [{ id: 2, partner_code: 'SUP-002', legal_name: '某某轴承厂', supplier_status: 'unknown_state' }]
|
||||||
|
})
|
||||||
|
const wrapper = mountPartner()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('unknown_state')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('新增伙伴不发生表单残留', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
partnersApi.list.mockReset().mockResolvedValue({
|
||||||
|
data: [{ id: 1, partner_code: 'SUP-001', legal_name: '某某钢材有限公司', supplier_status: 'active' }]
|
||||||
|
})
|
||||||
|
partnersApi.create.mockReset().mockResolvedValue({ data: {} })
|
||||||
|
partnersApi.update.mockReset().mockResolvedValue({ data: {} })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => { vi.restoreAllMocks() })
|
||||||
|
|
||||||
|
test('先编辑再新增时,请求体不能带上一条记录的 id', async () => {
|
||||||
|
const wrapper = mountPartner()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
// 先点「编辑」,让表单沾上 id=1
|
||||||
|
await findButton(wrapper, '编辑').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
// 关掉弹窗,再点「新增」
|
||||||
|
await findButton(wrapper, '取消').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
await openCreateDialog(wrapper, '供应商')
|
||||||
|
|
||||||
|
await inputByLabel(wrapper, '伙伴编码').setValue('SUP-009')
|
||||||
|
await inputByLabel(wrapper, '企业名称').setValue('某某轴承厂')
|
||||||
|
await clickSave(wrapper)
|
||||||
|
|
||||||
|
expect(partnersApi.update).not.toHaveBeenCalled()
|
||||||
|
expect(partnersApi.create).toHaveBeenCalledTimes(1)
|
||||||
|
const body = partnersApi.create.mock.calls[0][0]
|
||||||
|
expect(body.id).toBeUndefined()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<el-table-column prop="unified_credit_code" label="统一社会信用代码" min-width="180" />
|
<el-table-column prop="unified_credit_code" label="统一社会信用代码" min-width="180" />
|
||||||
<el-table-column prop="contact_name" label="联系人" width="110" />
|
<el-table-column prop="contact_name" label="联系人" width="110" />
|
||||||
<el-table-column prop="contact_phone" label="联系电话" width="140" />
|
<el-table-column prop="contact_phone" label="联系电话" width="140" />
|
||||||
<el-table-column :prop="statusField" label="合作状态" width="110" />
|
<el-table-column :prop="statusField" label="合作状态" width="110" :formatter="statusFormatter" />
|
||||||
<el-table-column label="操作" width="100"><template #default="{ row }"><el-button link type="primary" @click="openEdit(row)">编辑</el-button></template></el-table-column>
|
<el-table-column label="操作" width="100"><template #default="{ row }"><el-button link type="primary" @click="openEdit(row)">编辑</el-button></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-empty v-if="!loading && rows.length === 0" :description="`暂无${title}`" />
|
<el-empty v-if="!loading && rows.length === 0" :description="`暂无${title}`" />
|
||||||
|
|
@ -25,11 +25,7 @@
|
||||||
<el-form-item label="地址"><el-input v-model="form.address" /></el-form-item>
|
<el-form-item label="地址"><el-input v-model="form.address" /></el-form-item>
|
||||||
<el-form-item label="合作状态">
|
<el-form-item label="合作状态">
|
||||||
<el-select v-model="form.status" style="width: 100%">
|
<el-select v-model="form.status" style="width: 100%">
|
||||||
<el-option label="启用" value="active" />
|
<el-option v-for="(label, value) in STATUS_LABELS" :key="value" :label="label" :value="value" />
|
||||||
<el-option label="暂停" value="suspended" />
|
|
||||||
<el-option label="淘汰" value="eliminated" />
|
|
||||||
<el-option label="待审核" value="pending" />
|
|
||||||
<el-option label="停用" value="inactive" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
@ -42,6 +38,7 @@
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { partnersApi } from '../../api/partners'
|
import { partnersApi } from '../../api/partners'
|
||||||
|
import { replaceAllKeys } from '../../utils/replaceAllKeys'
|
||||||
|
|
||||||
const props = defineProps({ role: { type: String, required: true }, title: { type: String, required: true } })
|
const props = defineProps({ role: { type: String, required: true }, title: { type: String, required: true } })
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
|
|
@ -50,11 +47,17 @@ const saving = ref(false)
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingId = ref(null)
|
const editingId = ref(null)
|
||||||
const statusField = computed(() => `${props.role}_status`)
|
const statusField = computed(() => `${props.role}_status`)
|
||||||
|
// 合作状态英文枚举到中文的对照。列表列与编辑弹窗共用这一份,避免两处各写一遍后失步。
|
||||||
|
const STATUS_LABELS = { active: '启用', suspended: '暂停', eliminated: '淘汰', pending: '待审核', inactive: '停用' }
|
||||||
|
// 未收录的状态原样显示,不伪装成某个已知状态。
|
||||||
|
function statusFormatter(row, column, cellValue) { return STATUS_LABELS[cellValue] || cellValue }
|
||||||
const form = reactive(emptyForm())
|
const form = reactive(emptyForm())
|
||||||
|
|
||||||
function emptyForm() { return { partner_code: '', legal_name: '', unified_credit_code: '', contact_name: '', contact_phone: '', address: '', status: 'active', is_supplier: false, is_customer: false } }
|
function emptyForm() { return { partner_code: '', legal_name: '', unified_credit_code: '', contact_name: '', contact_phone: '', address: '', status: 'active', is_supplier: false, is_customer: false } }
|
||||||
async function load() { loading.value = true; try { const response = await partnersApi.list(props.role); rows.value = response.data || [] } finally { loading.value = false } }
|
async function load() { loading.value = true; try { const response = await partnersApi.list(props.role); rows.value = response.data || [] } finally { loading.value = false } }
|
||||||
function openCreate() { editingId.value = null; Object.assign(form, emptyForm()); dialogVisible.value = true }
|
// 用 replaceAllKeys 而不是 Object.assign:后者删不掉上次编辑残留的 id,
|
||||||
|
// 新增时会把别人的 id 一起提交上去。
|
||||||
|
function openCreate() { editingId.value = null; replaceAllKeys(form, emptyForm()); dialogVisible.value = true }
|
||||||
function openEdit(row) { editingId.value = row.id; Object.assign(form, row, { status: row[statusField.value] || 'active' }); dialogVisible.value = true }
|
function openEdit(row) { editingId.value = row.id; Object.assign(form, row, { status: row[statusField.value] || 'active' }); dialogVisible.value = true }
|
||||||
function payload() {
|
function payload() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ func (h *Handler) CreateMaterialCategory(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.OrganizationID = orgID
|
item.OrganizationID = orgID
|
||||||
if err := h.masterDataSvc.SaveCategory(&item); err != nil {
|
if err := h.masterDataSvc.CreateCategory(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ func (h *Handler) UpdateMaterialCategory(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.ID, item.OrganizationID = id, orgID
|
item.ID, item.OrganizationID = id, orgID
|
||||||
if err := h.masterDataSvc.SaveCategory(&item); err != nil {
|
if err := h.masterDataSvc.UpdateCategory(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +182,7 @@ func (h *Handler) CreateUnit(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.OrganizationID = orgID
|
item.OrganizationID = orgID
|
||||||
if err := h.masterDataSvc.SaveUnit(&item); err != nil {
|
if err := h.masterDataSvc.CreateUnit(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +202,7 @@ func (h *Handler) UpdateUnit(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.ID, item.OrganizationID = id, orgID
|
item.ID, item.OrganizationID = id, orgID
|
||||||
if err := h.masterDataSvc.SaveUnit(&item); err != nil {
|
if err := h.masterDataSvc.UpdateUnit(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +251,7 @@ func (h *Handler) CreateCodeRule(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.OrganizationID = orgID
|
item.OrganizationID = orgID
|
||||||
if err := h.masterDataSvc.SaveCodeRule(&item); err != nil {
|
if err := h.masterDataSvc.CreateCodeRule(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +271,7 @@ func (h *Handler) UpdateCodeRule(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.ID, item.OrganizationID = id, orgID
|
item.ID, item.OrganizationID = id, orgID
|
||||||
if err := h.masterDataSvc.SaveCodeRule(&item); err != nil {
|
if err := h.masterDataSvc.UpdateCodeRule(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -300,7 +300,7 @@ func (h *Handler) CreatePartner(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.OrganizationID = orgID
|
item.OrganizationID = orgID
|
||||||
if err := h.partnerSvc.Save(&item); err != nil {
|
if err := h.partnerSvc.Create(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -320,7 +320,7 @@ func (h *Handler) UpdatePartner(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.ID, item.OrganizationID = id, orgID
|
item.ID, item.OrganizationID = id, orgID
|
||||||
if err := h.partnerSvc.Save(&item); err != nil {
|
if err := h.partnerSvc.Update(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -349,7 +349,7 @@ func (h *Handler) CreateWarehouse(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.OrganizationID = orgID
|
item.OrganizationID = orgID
|
||||||
if err := h.warehouseSvc.Save(&item); err != nil {
|
if err := h.warehouseSvc.Create(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +369,7 @@ func (h *Handler) UpdateWarehouse(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.ID, item.OrganizationID = id, orgID
|
item.ID, item.OrganizationID = id, orgID
|
||||||
if err := h.warehouseSvc.Save(&item); err != nil {
|
if err := h.warehouseSvc.Update(&item); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +389,7 @@ func (h *Handler) CreateWarehouseLocation(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.WarehouseID = warehouseID
|
item.WarehouseID = warehouseID
|
||||||
if err := h.warehouseSvc.SaveLocation(&item, orgID); err != nil {
|
if err := h.warehouseSvc.CreateLocation(&item, orgID); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -414,7 +414,7 @@ func (h *Handler) UpdateWarehouseLocation(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.ID, item.WarehouseID = locationID, warehouseID
|
item.ID, item.WarehouseID = locationID, warehouseID
|
||||||
if err := h.warehouseSvc.SaveLocation(&item, orgID); err != nil {
|
if err := h.warehouseSvc.UpdateLocation(&item, orgID); err != nil {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var errInvalidMasterData = errors.New("code and name are required")
|
var errInvalidMasterData = errors.New("code and name are required")
|
||||||
|
var errUpdateRequiresID = errors.New("id is required for update")
|
||||||
var ErrProtectedRole = repository.ErrProtectedRole
|
var ErrProtectedRole = repository.ErrProtectedRole
|
||||||
|
|
||||||
type OrganizationService struct {
|
type OrganizationService struct {
|
||||||
|
|
@ -67,7 +68,29 @@ func NewMasterDataService() *MasterDataService {
|
||||||
func (s *MasterDataService) ListCategories(orgID int64) ([]model.MaterialCategory, error) {
|
func (s *MasterDataService) ListCategories(orgID int64) ([]model.MaterialCategory, error) {
|
||||||
return s.repo.ListCategories(orgID)
|
return s.repo.ListCategories(orgID)
|
||||||
}
|
}
|
||||||
func (s *MasterDataService) SaveCategory(item *model.MaterialCategory) error {
|
|
||||||
|
// CreateCategory 新建分类。请求体可能带着前端表单残留的 id,这里不读取 item.ID,
|
||||||
|
// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。
|
||||||
|
func (s *MasterDataService) CreateCategory(item *model.MaterialCategory) error {
|
||||||
|
if err := prepareCategory(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.CreateCategory(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategory 更新分类。id 来自路由参数,必须显式存在。
|
||||||
|
func (s *MasterDataService) UpdateCategory(item *model.MaterialCategory) error {
|
||||||
|
if item.ID == 0 {
|
||||||
|
return errUpdateRequiresID
|
||||||
|
}
|
||||||
|
if err := prepareCategory(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.UpdateCategory(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareCategory 是新建与更新共用的字段清理、校验和默认值补全。
|
||||||
|
func prepareCategory(item *model.MaterialCategory) error {
|
||||||
item.Code = strings.TrimSpace(item.Code)
|
item.Code = strings.TrimSpace(item.Code)
|
||||||
item.Name = strings.TrimSpace(item.Name)
|
item.Name = strings.TrimSpace(item.Name)
|
||||||
item.Description = strings.TrimSpace(item.Description)
|
item.Description = strings.TrimSpace(item.Description)
|
||||||
|
|
@ -77,15 +100,33 @@ func (s *MasterDataService) SaveCategory(item *model.MaterialCategory) error {
|
||||||
if item.Status == "" {
|
if item.Status == "" {
|
||||||
item.Status = "active"
|
item.Status = "active"
|
||||||
}
|
}
|
||||||
if item.ID == 0 {
|
return nil
|
||||||
return s.repo.CreateCategory(item)
|
|
||||||
}
|
|
||||||
return s.repo.UpdateCategory(item)
|
|
||||||
}
|
}
|
||||||
func (s *MasterDataService) ListUnits(orgID int64) ([]model.Unit, error) {
|
func (s *MasterDataService) ListUnits(orgID int64) ([]model.Unit, error) {
|
||||||
return s.repo.ListUnits(orgID)
|
return s.repo.ListUnits(orgID)
|
||||||
}
|
}
|
||||||
func (s *MasterDataService) SaveUnit(item *model.Unit) error {
|
|
||||||
|
// CreateUnit 新建单位。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。
|
||||||
|
func (s *MasterDataService) CreateUnit(item *model.Unit) error {
|
||||||
|
if err := prepareUnit(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.CreateUnit(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUnit 更新单位。id 来自路由参数,必须显式存在。
|
||||||
|
func (s *MasterDataService) UpdateUnit(item *model.Unit) error {
|
||||||
|
if item.ID == 0 {
|
||||||
|
return errUpdateRequiresID
|
||||||
|
}
|
||||||
|
if err := prepareUnit(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.UpdateUnit(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareUnit 是新建与更新共用的字段清理、校验和默认值补全。
|
||||||
|
func prepareUnit(item *model.Unit) error {
|
||||||
item.Code = strings.TrimSpace(item.Code)
|
item.Code = strings.TrimSpace(item.Code)
|
||||||
item.Name = strings.TrimSpace(item.Name)
|
item.Name = strings.TrimSpace(item.Name)
|
||||||
if item.Code == "" || item.Name == "" {
|
if item.Code == "" || item.Name == "" {
|
||||||
|
|
@ -97,10 +138,7 @@ func (s *MasterDataService) SaveUnit(item *model.Unit) error {
|
||||||
if item.PrecisionScale < 0 || item.PrecisionScale > 6 {
|
if item.PrecisionScale < 0 || item.PrecisionScale > 6 {
|
||||||
return errors.New("precision_scale must be between 0 and 6")
|
return errors.New("precision_scale must be between 0 and 6")
|
||||||
}
|
}
|
||||||
if item.ID == 0 {
|
return nil
|
||||||
return s.repo.CreateUnit(item)
|
|
||||||
}
|
|
||||||
return s.repo.UpdateUnit(item)
|
|
||||||
}
|
}
|
||||||
func (s *MasterDataService) ListCodeRules(orgID int64) ([]model.CodeRule, error) {
|
func (s *MasterDataService) ListCodeRules(orgID int64) ([]model.CodeRule, error) {
|
||||||
return s.repo.ListCodeRules(orgID)
|
return s.repo.ListCodeRules(orgID)
|
||||||
|
|
@ -118,7 +156,28 @@ func (s *MasterDataService) NextMaterialCode(orgID, categoryID int64) (*model.Ne
|
||||||
}
|
}
|
||||||
return &model.NextMaterialCode{Available: true, Code: rule.FormatCode(rule.NextSequence)}, nil
|
return &model.NextMaterialCode{Available: true, Code: rule.FormatCode(rule.NextSequence)}, nil
|
||||||
}
|
}
|
||||||
func (s *MasterDataService) SaveCodeRule(item *model.CodeRule) error {
|
|
||||||
|
// CreateCodeRule 新建编码规则。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。
|
||||||
|
func (s *MasterDataService) CreateCodeRule(item *model.CodeRule) error {
|
||||||
|
if err := prepareCodeRule(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.CreateCodeRule(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCodeRule 更新编码规则。id 来自路由参数,必须显式存在。
|
||||||
|
func (s *MasterDataService) UpdateCodeRule(item *model.CodeRule) error {
|
||||||
|
if item.ID == 0 {
|
||||||
|
return errUpdateRequiresID
|
||||||
|
}
|
||||||
|
if err := prepareCodeRule(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.UpdateCodeRule(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareCodeRule 是新建与更新共用的字段清理、校验和默认值补全。
|
||||||
|
func prepareCodeRule(item *model.CodeRule) error {
|
||||||
item.Prefix = strings.TrimSpace(item.Prefix)
|
item.Prefix = strings.TrimSpace(item.Prefix)
|
||||||
if item.CategoryID <= 0 || item.Prefix == "" {
|
if item.CategoryID <= 0 || item.Prefix == "" {
|
||||||
return errors.New("category_id and prefix are required")
|
return errors.New("category_id and prefix are required")
|
||||||
|
|
@ -132,10 +191,7 @@ func (s *MasterDataService) SaveCodeRule(item *model.CodeRule) error {
|
||||||
if item.Status == "" {
|
if item.Status == "" {
|
||||||
item.Status = "active"
|
item.Status = "active"
|
||||||
}
|
}
|
||||||
if item.ID == 0 {
|
return nil
|
||||||
return s.repo.CreateCodeRule(item)
|
|
||||||
}
|
|
||||||
return s.repo.UpdateCodeRule(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartnerService struct{ repo *repository.PartnerRepository }
|
type PartnerService struct{ repo *repository.PartnerRepository }
|
||||||
|
|
@ -149,7 +205,29 @@ func (s *PartnerService) List(orgID int64, role string) ([]model.BusinessPartner
|
||||||
}
|
}
|
||||||
return s.repo.List(orgID, role)
|
return s.repo.List(orgID, role)
|
||||||
}
|
}
|
||||||
func (s *PartnerService) Save(item *model.BusinessPartner) error {
|
|
||||||
|
// Create 新建伙伴。请求体可能带着前端表单残留的 id,这里不读取 item.ID,
|
||||||
|
// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。
|
||||||
|
func (s *PartnerService) Create(item *model.BusinessPartner) error {
|
||||||
|
if err := preparePartner(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新伙伴。id 来自路由参数,必须显式存在。
|
||||||
|
func (s *PartnerService) Update(item *model.BusinessPartner) error {
|
||||||
|
if item.ID == 0 {
|
||||||
|
return errUpdateRequiresID
|
||||||
|
}
|
||||||
|
if err := preparePartner(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.Update(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// preparePartner 是新建与更新共用的字段清理、校验和默认值补全。
|
||||||
|
func preparePartner(item *model.BusinessPartner) error {
|
||||||
item.PartnerCode = strings.TrimSpace(item.PartnerCode)
|
item.PartnerCode = strings.TrimSpace(item.PartnerCode)
|
||||||
item.LegalName = strings.TrimSpace(item.LegalName)
|
item.LegalName = strings.TrimSpace(item.LegalName)
|
||||||
if item.PartnerCode == "" || item.LegalName == "" {
|
if item.PartnerCode == "" || item.LegalName == "" {
|
||||||
|
|
@ -164,10 +242,7 @@ func (s *PartnerService) Save(item *model.BusinessPartner) error {
|
||||||
if item.IsCustomer && item.CustomerStatus == "" {
|
if item.IsCustomer && item.CustomerStatus == "" {
|
||||||
item.CustomerStatus = "active"
|
item.CustomerStatus = "active"
|
||||||
}
|
}
|
||||||
if item.ID == 0 {
|
return nil
|
||||||
return s.repo.Create(item)
|
|
||||||
}
|
|
||||||
return s.repo.Update(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseService struct {
|
type WarehouseService struct {
|
||||||
|
|
@ -180,7 +255,28 @@ func NewWarehouseService() *WarehouseService {
|
||||||
func (s *WarehouseService) List(orgID int64) ([]model.Warehouse, error) {
|
func (s *WarehouseService) List(orgID int64) ([]model.Warehouse, error) {
|
||||||
return s.repo.List(orgID)
|
return s.repo.List(orgID)
|
||||||
}
|
}
|
||||||
func (s *WarehouseService) Save(item *model.Warehouse) error {
|
|
||||||
|
// Create 新建仓库。不读取 item.ID,脏 id 不会把新建变成覆盖。
|
||||||
|
func (s *WarehouseService) Create(item *model.Warehouse) error {
|
||||||
|
if err := prepareWarehouse(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.Create(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新仓库。id 来自路由参数,必须显式存在。
|
||||||
|
func (s *WarehouseService) Update(item *model.Warehouse) error {
|
||||||
|
if item.ID == 0 {
|
||||||
|
return errUpdateRequiresID
|
||||||
|
}
|
||||||
|
if err := prepareWarehouse(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.Update(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareWarehouse 是新建与更新共用的字段清理、校验和默认值补全。
|
||||||
|
func prepareWarehouse(item *model.Warehouse) error {
|
||||||
item.Code = strings.TrimSpace(item.Code)
|
item.Code = strings.TrimSpace(item.Code)
|
||||||
item.Name = strings.TrimSpace(item.Name)
|
item.Name = strings.TrimSpace(item.Name)
|
||||||
if item.Code == "" || item.Name == "" {
|
if item.Code == "" || item.Name == "" {
|
||||||
|
|
@ -189,12 +285,30 @@ func (s *WarehouseService) Save(item *model.Warehouse) error {
|
||||||
if item.Status == "" {
|
if item.Status == "" {
|
||||||
item.Status = "active"
|
item.Status = "active"
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLocation 新建库位。orgID 用于校验仓库归属,item.ID 不参与。
|
||||||
|
func (s *WarehouseService) CreateLocation(item *model.WarehouseLocation, orgID int64) error {
|
||||||
|
if err := prepareLocation(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.repo.CreateLocation(item, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLocation 更新库位。id 来自路由参数,必须显式存在。
|
||||||
|
func (s *WarehouseService) UpdateLocation(item *model.WarehouseLocation, orgID int64) error {
|
||||||
if item.ID == 0 {
|
if item.ID == 0 {
|
||||||
return s.repo.Create(item)
|
return errUpdateRequiresID
|
||||||
}
|
}
|
||||||
return s.repo.Update(item)
|
if err := prepareLocation(item); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
func (s *WarehouseService) SaveLocation(item *model.WarehouseLocation, orgID int64) error {
|
return s.repo.UpdateLocation(item, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareLocation 是新建与更新共用的字段清理、校验和默认值补全。
|
||||||
|
func prepareLocation(item *model.WarehouseLocation) error {
|
||||||
item.Code = strings.TrimSpace(item.Code)
|
item.Code = strings.TrimSpace(item.Code)
|
||||||
item.Name = strings.TrimSpace(item.Name)
|
item.Name = strings.TrimSpace(item.Name)
|
||||||
if item.Code == "" || item.Name == "" || item.WarehouseID <= 0 {
|
if item.Code == "" || item.Name == "" || item.WarehouseID <= 0 {
|
||||||
|
|
@ -203,8 +317,5 @@ func (s *WarehouseService) SaveLocation(item *model.WarehouseLocation, orgID int
|
||||||
if item.Status == "" {
|
if item.Status == "" {
|
||||||
item.Status = "active"
|
item.Status = "active"
|
||||||
}
|
}
|
||||||
if item.ID == 0 {
|
return nil
|
||||||
return s.repo.CreateLocation(item, orgID)
|
|
||||||
}
|
|
||||||
return s.repo.UpdateLocation(item, orgID)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
199
internal/service/admin_master_test.go
Normal file
199
internal/service/admin_master_test.go
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/wmms/wmms/internal/model"
|
||||||
|
"github.com/wmms/wmms/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 前端表单在「新增」时可能残留上次编辑过的 id(PartnerList.vue 的
|
||||||
|
// openCreate 只做 Object.assign、不删多余键)。创建必须无视这个 id,
|
||||||
|
// 否则会走成 UPDATE 把旧记录覆盖掉——这正是本次要修的缺陷。
|
||||||
|
func TestPartnerCreateInsertsEvenWhenItemCarriesStaleID(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
database.DB = db
|
||||||
|
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||||||
|
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectExec("INSERT INTO business_partners").WillReturnResult(sqlmock.NewResult(2, 1))
|
||||||
|
mock.ExpectExec("INSERT INTO organization_partners").WillReturnResult(sqlmock.NewResult(2, 1))
|
||||||
|
mock.ExpectCommit()
|
||||||
|
|
||||||
|
item := &model.BusinessPartner{ID: 1, PartnerCode: "SUP-002", LegalName: "新伙伴", IsSupplier: true}
|
||||||
|
if err := NewPartnerService().Create(item); err != nil {
|
||||||
|
t.Fatalf("Create: %v", err)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新接口漏传 id 时必须报错,否则会静默写 0 号记录(什么都不改)而不自知。
|
||||||
|
func TestPartnerUpdateRequiresID(t *testing.T) {
|
||||||
|
err := NewPartnerService().Update(&model.BusinessPartner{PartnerCode: "SUP-001", LegalName: "伙伴", IsSupplier: true})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("缺少 id 的更新应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拆方法不能把校验丢了。
|
||||||
|
func TestPartnerCreateStillValidatesName(t *testing.T) {
|
||||||
|
err := NewPartnerService().Create(&model.BusinessPartner{PartnerCode: "SUP-001", IsSupplier: true})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("legal_name 为空应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPartnerCreateStillRequiresARole(t *testing.T) {
|
||||||
|
err := NewPartnerService().Create(&model.BusinessPartner{PartnerCode: "SUP-001", LegalName: "伙伴"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("既非供应商也非客户时应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MasterDataSettings.vue 的 openCreate 同样是 Object.assign 复用表单,
|
||||||
|
// 分类新增也会带上前一次编辑残留的 id,必须走 INSERT。
|
||||||
|
func TestCategoryCreateInsertsEvenWhenItemCarriesStaleID(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
database.DB = db
|
||||||
|
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||||||
|
|
||||||
|
mock.ExpectExec("INSERT INTO material_categories").WillReturnResult(sqlmock.NewResult(9, 1))
|
||||||
|
|
||||||
|
item := &model.MaterialCategory{ID: 1, Code: "A99", Name: "新分类"}
|
||||||
|
if err := NewMasterDataService().CreateCategory(item); err != nil {
|
||||||
|
t.Fatalf("CreateCategory: %v", err)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnitUpdateRequiresID(t *testing.T) {
|
||||||
|
err := NewMasterDataService().UpdateUnit(&model.Unit{Code: "pcs", Name: "件"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("缺少 id 的更新应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodeRuleUpdateRequiresID(t *testing.T) {
|
||||||
|
err := NewMasterDataService().UpdateCodeRule(&model.CodeRule{CategoryID: 1, Prefix: "A", SequenceLength: 4})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("缺少 id 的更新应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单位的小数位校验不能因为拆方法而丢失。
|
||||||
|
func TestUnitCreateStillValidatesPrecisionScale(t *testing.T) {
|
||||||
|
err := NewMasterDataService().CreateUnit(&model.Unit{Code: "kg", Name: "千克", PrecisionScale: 9})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("precision_scale 超过 6 应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarehouseCreateInsertsEvenWhenItemCarriesStaleID(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
database.DB = db
|
||||||
|
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||||||
|
|
||||||
|
mock.ExpectExec("INSERT INTO warehouses").WillReturnResult(sqlmock.NewResult(5, 1))
|
||||||
|
|
||||||
|
item := &model.Warehouse{ID: 3, Code: "WH2", Name: "二号仓"}
|
||||||
|
if err := NewWarehouseService().Create(item); err != nil {
|
||||||
|
t.Fatalf("Create: %v", err)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarehouseUpdateRequiresID(t *testing.T) {
|
||||||
|
err := NewWarehouseService().Update(&model.Warehouse{Code: "WH1", Name: "一号仓"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("缺少 id 的更新应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocationCreateRequiresWarehouseID(t *testing.T) {
|
||||||
|
err := NewWarehouseService().CreateLocation(&model.WarehouseLocation{Code: "L1", Name: "库位一"}, 1)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("缺少 warehouse_id 的库位创建应当被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上面的 UpdateRequiresID 只覆盖了拒绝分支,方法体里真正的 s.repo.Update 从未被执行。
|
||||||
|
// 六个 Update* 互为复制粘贴的模板,若某一处尾行被误写成 s.repo.Create,
|
||||||
|
// 生产行为会退化成「编辑即新增」——与本次修复的缺陷同级。这里补上走通的路径。
|
||||||
|
func TestPartnerUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
database.DB = db
|
||||||
|
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||||||
|
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectQuery("SELECT business_partner_id FROM organization_partners").
|
||||||
|
WithArgs(int64(12), int64(1)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"business_partner_id"}).AddRow(int64(7)))
|
||||||
|
mock.ExpectExec("UPDATE business_partners").WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
mock.ExpectExec("UPDATE organization_partners").WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
mock.ExpectCommit()
|
||||||
|
|
||||||
|
item := &model.BusinessPartner{
|
||||||
|
ID: 12,
|
||||||
|
OrganizationID: 1,
|
||||||
|
PartnerCode: "SUP-007",
|
||||||
|
LegalName: "伙伴改名",
|
||||||
|
IsSupplier: true,
|
||||||
|
}
|
||||||
|
if err := NewPartnerService().Update(item); err != nil {
|
||||||
|
t.Fatalf("Update: %v", err)
|
||||||
|
}
|
||||||
|
// 只登记了 SELECT ... FOR UPDATE 与两条 UPDATE。若尾行被误写成 Create,
|
||||||
|
// 事务里发出的 INSERT 与这里的期望不匹配:要么 Create 直接报错、要么
|
||||||
|
// 这两条 UPDATE 永不被消费,ExpectationsWereMet 必然失败。
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("应当执行 UPDATE,实际 SQL 不是更新: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCategoryUpdateUpdatesInsteadOfInserting(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
database.DB = db
|
||||||
|
t.Cleanup(func() { database.DB = nil; db.Close() })
|
||||||
|
|
||||||
|
mock.ExpectExec("UPDATE material_categories").
|
||||||
|
WithArgs("改名后的分类", "说明", nil, "active", int64(9), int64(1)).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
|
item := &model.MaterialCategory{
|
||||||
|
ID: 9,
|
||||||
|
OrganizationID: 1,
|
||||||
|
Code: "A01",
|
||||||
|
Name: "改名后的分类",
|
||||||
|
Description: "说明",
|
||||||
|
}
|
||||||
|
if err := NewMasterDataService().UpdateCategory(item); err != nil {
|
||||||
|
t.Fatalf("UpdateCategory: %v", err)
|
||||||
|
}
|
||||||
|
// 这里只登记了 UPDATE;误写成 Create 时发出的 INSERT 不会被消费,
|
||||||
|
// Expectation 落空,ExpectationsWereMet 报错。
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("应当执行 UPDATE,实际 SQL 不是插入: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user