diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json new file mode 100644 index 0000000..a4b647d --- /dev/null +++ b/.workbuddy/expert-history.json @@ -0,0 +1,17 @@ +{ + "version": 2, + "sessions": { + "81d18d7cb0a14f7b80ab19186392c337": [ + { + "expertId": "BackendArchitect", + "name": "Joy", + "profession": "后端架构师", + "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/BackendArchitect/BackendArchitect.png", + "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/BackendArchitect/BackendArchitect_zh.md", + "usedAt": 1776079998628, + "industryId": "all" + } + ] + }, + "lastUpdated": 1776081278156 +} \ No newline at end of file diff --git a/.workbuddy/memory/2026-04-13.md b/.workbuddy/memory/2026-04-13.md new file mode 100644 index 0000000..594000e --- /dev/null +++ b/.workbuddy/memory/2026-04-13.md @@ -0,0 +1,26 @@ +# 2026-04-13 + +## ops2 项目采购模块改造 + +为采购订单增加状态管理功能,涉及前后端多处改动: + +### 后端改动 +- `TabPurchaseOrder` 新增 `OrderStatus` 字段(pending/ordered/arrived/received),默认 pending +- `TabPurchaseOrder` 新增 `UpdatedAt` 字段(GORM autoUpdateTime) +- `TabPurchaseCosts` 新增 `CurrencyType`(1-CNY 2-MOP 3-HKD 4-USD)和 `CostType`(1-单价 2-运费) +- 新表 `TabPurchaseCommit`:记录每次状态变更(Action: create/create_status)、旧状态、新状态、评论、操作IP +- `CostItem` 前端结构体 `CurrencyType` 和 `Type` 从 string 改为 int(与数据库一致) +- `/purchase/getorder` 新增返回 `commits`(状态变更记录列表) +- `/purchase/updatestatus` 新接口:更新订单状态,可选附带 comment,写入 TabPurchaseCommit + TabPurchaseLog +- `addorder` 创建时自动写入第一条 commit(状态 pending) +- 状态值白名单校验:只允许 pending/ordered/arrived/received + +### 前端改动 +- `api/purchase.js` 新增 `updateOrderStatus(id, status, comment)` 方法 +- `PurchaseList.vue` 列表页去掉假数据列,显示真实标题/备注/创建时间,状态用彩色标签(黄-待处理、蓝-已下单、紫-已到达、绿-已收件) +- `ShowOrder.vue` 详情页新增:状态快捷切换按钮(四个状态一键切换)、状态变更 commit 历史时间线(竖排列表,含状态标签+时间+评论) +- i18n 同步新增 8 个翻译 key + +### 注意事项 +- 重启后端后 GORM AutoMigrate 会自动新增字段和表,无需手动 SQL +- 前端 `CostItem` 的 CurrencyType/Type 改为 int,与后端一致 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md new file mode 100644 index 0000000..e69de29 diff --git a/backend/my_work/routers/apiPurchase.go b/backend/my_work/routers/apiPurchase.go index cca99cf..02269c7 100644 --- a/backend/my_work/routers/apiPurchase.go +++ b/backend/my_work/routers/apiPurchase.go @@ -12,11 +12,11 @@ import ( ) type CostItem struct { - Cost int `json:"cost"` // 费用 + Cost int `json:"cost"` // 费用(分) CostT int `json:"costt"` // 总价 - CurrencyType string `json:"currencytype"` // 货币类型 + CurrencyType int `json:"currencytype"` // 货币类型: 1-CNY 2-MOP 3-HKD 4-USD Int int `json:"int"` // 数量 - Type string `json:"type"` // 费用类型 + Type int `json:"type"` // 费用类型: 1-单价 2-运费 } type From_purchase_addorder struct { Costs []CostItem `json:"costs"` // 成本 @@ -32,22 +32,26 @@ type From_purchase_addorder struct { } type TabPurchaseOrder struct { - ID uint `gorm:"primarykey"` - UserID uint `gorm:"not null"` - Title string `gorm:"size:200;comment:标题"` - Remark string `gorm:"type:text;comment:备注"` - Link string `gorm:"size:1000;comment:链接"` - Styles string `gorm:"type:text;comment:样式数组"` - CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` - DeletedAt gorm.DeletedAt `gorm:"index"` + ID uint `gorm:"primarykey"` + UserID uint `gorm:"not null"` + Title string `gorm:"size:200;comment:标题"` + Remark string `gorm:"type:text;comment:备注"` + Link string `gorm:"size:1000;comment:链接"` + Styles string `gorm:"type:text;comment:样式数组"` + OrderStatus string `gorm:"size:50;default:pending;comment:订单状态: pending-待处理 ordered-已下单 arrived-已到达 received-已收件"` + CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` + UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index"` } type TabPurchaseCosts struct { - ID uint `gorm:"primarykey"` - OrderID uint `gorm:"not null"` - UserID uint `gorm:"not null"` - Price int `gorm:"not null"` - Quantity int `gorm:"not null"` + ID uint `gorm:"primarykey"` + OrderID uint `gorm:"not null"` + UserID uint `gorm:"not null"` + Price int `gorm:"not null"` + Quantity int `gorm:"not null"` + CurrencyType int `gorm:"default:1;comment:货币类型: 1-CNY 2-MOP 3-HKD 4-USD"` + CostType int `gorm:"default:1;comment:费用类型: 1-单价 2-运费"` CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` } @@ -58,6 +62,19 @@ type TabPurchaseFileBind struct { CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` } +// TabPurchaseCommit 记录订单状态变更及评论 +type TabPurchaseCommit struct { + ID uint `gorm:"primarykey"` + OrderID uint `gorm:"not null;index;comment:关联订单ID"` + UserID uint `gorm:"not null;comment:操作人ID"` + Action string `gorm:"size:50;not null;comment:操作类型: create-创建 create_status-状态变更"` + Status string `gorm:"size:50;comment:变更后的状态"` + OldStatus string `gorm:"size:50;comment:变更前的状态"` + Comment string `gorm:"type:text;comment:评论/备注"` + IP string `gorm:"size:50;comment:操作IP"` + CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"` +} + type TabPurchaseLog struct { ID uint `gorm:"primarykey"` OrderID uint `gorm:"not null;index;comment:关联OrderID"` @@ -77,11 +94,152 @@ func ApiPurchaseInit() { models.DB.AutoMigrate(&TabPurchaseCosts{}) models.DB.AutoMigrate(&TabPurchaseFileBind{}) models.DB.AutoMigrate(&TabPurchaseLog{}) + models.DB.AutoMigrate(&TabPurchaseCommit{}) } func ApiPurchase(r *gin.RouterGroup) { + r.POST("/getorder", func(ctx *gin.Context) { + isAuth, _, data := AuthenticationAuthority(ctx) + if !isAuth { + ReturnJson(ctx, "userCookieError", nil) + return + } + + type FromGetOrder struct { + ID uint `json:"id"` + } + var from FromGetOrder + if err := mapstructure.Decode(data, &from); err != nil || from.ID == 0 { + ReturnJson(ctx, "jsonErr", nil) + return + } + + var order TabPurchaseOrder + if err := models.DB.Where("id = ?", from.ID).First(&order).Error; err != nil { + ReturnJson(ctx, "order_not_found", nil) + return + } + + // 查询关联费用 + var costs []TabPurchaseCosts + models.DB.Where("order_id = ?", from.ID).Find(&costs) + + // 查询关联图片 + var binds []TabPurchaseFileBind + models.DB.Where("order_id = ?", from.ID).Find(&binds) + var fileIDs []uint + for _, b := range binds { + fileIDs = append(fileIDs, b.FileID) + } + var files []models.TabFileInfo_ + if len(fileIDs) > 0 { + models.DB.Where("id IN ?", fileIDs).Find(&files) + } + + // 查询状态变更记录 + var commits []TabPurchaseCommit + models.DB.Where("order_id = ?", from.ID).Order("created_at DESC").Find(&commits) + + ReturnJson(ctx, "apiOK", gin.H{ + "order": order, + "costs": costs, + "photos": files, + "commits": commits, + }) + }) + + // 更新订单状态(可附带评论) + r.POST("/updatestatus", func(ctx *gin.Context) { + isAuth, user, data := AuthenticationAuthority(ctx) + if !isAuth { + ReturnJson(ctx, "userCookieError", nil) + return + } + + type FromUpdateStatus struct { + ID uint `json:"id"` + Status string `json:"status" binding:"required"` + Comment string `json:"comment"` + } + var from FromUpdateStatus + if err := mapstructure.Decode(data, &from); err != nil || from.ID == 0 { + ReturnJson(ctx, "jsonErr", nil) + return + } + + // 校验状态值 + validStatuses := map[string]bool{ + "pending": true, + "ordered": true, + "arrived": true, + "received": true, + } + if !validStatuses[from.Status] { + ReturnJson(ctx, "invalid_status", nil) + return + } + + var order TabPurchaseOrder + if err := models.DB.Where("id = ?", from.ID).First(&order).Error; err != nil { + ReturnJson(ctx, "order_not_found", nil) + return + } + + oldStatus := order.OrderStatus + if oldStatus == from.Status { + ReturnJson(ctx, "status_no_change", nil) + return + } + + // 更新状态 + updates := map[string]interface{}{ + "order_status": from.Status, + } + if err := models.DB.Model(&order).Updates(updates).Error; err != nil { + ReturnJson(ctx, "apiErr", nil) + return + } + + // 写状态变更 commit + comment := from.Comment + if comment == "" { + comment = "状态变更为: " + from.Status + } + commit := TabPurchaseCommit{ + OrderID: order.ID, + UserID: user.ID, + Action: "create_status", + Status: from.Status, + OldStatus: oldStatus, + Comment: comment, + IP: ctx.ClientIP(), + } + models.DB.Create(&commit) + + // 写操作日志 + newContent, _ := json.Marshal(map[string]string{ + "status": from.Status, + "comment": comment, + }) + oldContent, _ := json.Marshal(map[string]string{ + "status": oldStatus, + }) + tosqllog := TabPurchaseLog{ + UserID: user.ID, + OrderID: order.ID, + ActionType: "update_status", + NewContent: string(newContent), + OldContent: string(oldContent), + IP: ctx.ClientIP(), + Remark: comment, + } + models.DB.Create(&tosqllog) + + ReturnJson(ctx, "apiOK", nil) + }) + r.POST("/getorders", func(ctx *gin.Context) { isAuth, _, data := AuthenticationAuthority(ctx) if isAuth { @@ -185,59 +343,79 @@ func ApiPurchase(r *gin.RouterGroup) { // fmt.Println("err5") // } - if is_data_ok { - //校验通过 - //载入数据库 + if is_data_ok { + //校验通过 + //photos, _ := json.Marshal(jsondata.Photos) + new_data := TabPurchaseOrder{ + UserID: user.ID, + Title: jsondata.Title, + Remark: jsondata.Remark, + Link: jsondata.Link, + Styles: jsondata.Styles, + OrderStatus: "pending", // 默认待处理 + } + models.DB.Create(&new_data) - //fmt.Println("yes") - - //photos, _ := json.Marshal(jsondata.Photos) //把图片数组转换成字符串 - new_data := TabPurchaseOrder{ - UserID: user.ID, - Title: jsondata.Title, - Remark: jsondata.Remark, - Link: jsondata.Link, - Styles: jsondata.Styles, + for i := 0; i < len(jsondata.Costs); i++ { + currencyType := jsondata.Costs[i].CurrencyType + if currencyType <= 0 { + currencyType = 1 // 默认 CNY } - models.DB.Create(&new_data) + costType := jsondata.Costs[i].Type + if costType <= 0 { + costType = 1 // 默认单价 + } + new_cost_data := TabPurchaseCosts{ + Price: jsondata.Costs[i].Cost, + Quantity: jsondata.Costs[i].Int, + UserID: user.ID, + OrderID: new_data.ID, + CurrencyType: currencyType, + CostType: costType, + } + models.DB.Create(&new_cost_data) + } - for i := 0; i < len(jsondata.Costs); i++ { - new_cost_data := TabPurchaseCosts{ - Price: jsondata.Costs[i].Cost, - Quantity: jsondata.Costs[i].Int, - UserID: user.ID, - OrderID: new_data.ID, + //绑定文件 + for i := 0; i < len(jsondata.Photos); i++ { + findFile := models.TabFileInfo_{ + Sha256: jsondata.Photos[i], + Type: "image", + } + if models.DB.Where(&findFile).First(&findFile).Error == nil { + bind := TabPurchaseFileBind{ + OrderID: new_data.ID, + FileID: findFile.ID, } - models.DB.Create(&new_cost_data) + models.DB.Create(&bind) } + } - //绑定文件 - for i := 0; i < len(jsondata.Photos); i++ { - findFile := models.TabFileInfo_{ - Sha256: jsondata.Photos[i], - Type: "image", - } - if models.DB.Where(&findFile).First(&findFile).Error == nil { - bind := TabPurchaseFileBind{ - OrderID: new_data.ID, - FileID: findFile.ID, - } - models.DB.Create(&bind) - } + // 写创建日志 + newContent, _ := json.Marshal(jsondata) + tosqllog := TabPurchaseLog{ + UserID: user.ID, + OrderID: new_data.ID, + ActionType: "create", + NewContent: string(newContent), + OldContent: "", + IP: ctx.ClientIP(), + } + models.DB.Create(&tosqllog) - } - newContent, _ := json.Marshal(jsondata) // 👈 转 JSON - tosqllog := TabPurchaseLog{ - UserID: user.ID, - OrderID: new_data.ID, - ActionType: "create", - NewContent: string(newContent), // 👈 直接赋值 - OldContent: "", - IP: ctx.ClientIP(), - } - models.DB.Debug().Create(&tosqllog) + // 写状态创建 commit + commitLog := TabPurchaseCommit{ + OrderID: new_data.ID, + UserID: user.ID, + Action: "create", + Status: "pending", + OldStatus: "", + Comment: "订单创建", + IP: ctx.ClientIP(), + } + models.DB.Create(&commitLog) - ReturnJson(ctx, "apiOK", nil) + ReturnJson(ctx, "apiOK", nil) } else { ReturnJson(ctx, "jsonErr_1", nil) diff --git a/frontend/ops_vue_js/src/api/purchase.js b/frontend/ops_vue_js/src/api/purchase.js index 06bc5c4..2a8c1b4 100644 --- a/frontend/ops_vue_js/src/api/purchase.js +++ b/frontend/ops_vue_js/src/api/purchase.js @@ -10,4 +10,14 @@ export const purchaseApi = { addOrder(data) { return api.post('/purchase/addorder', data) }, + + /** 获取单个订单详情(包含费用明细、图片、状态变更记录) */ + getOrder(id) { + return api.post('/purchase/getorder', { id }) + }, + + /** 更新订单状态(可附带评论) */ + updateOrderStatus(id, status, comment = '') { + return api.post('/purchase/updatestatus', { id, status, comment }) + }, } diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index 66a09ce..d6b7f69 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -66,6 +66,10 @@ "created_at": "Created At", "updated_at": "Updated At", "status": "Status", + "status_pending": "Pending", + "status_ordered": "Ordered", + "status_arrived": "Arrived", + "status_received": "Received", "completed": "Completed", "pending": "Pending", "show": "Show", @@ -74,7 +78,23 @@ "add_part": "Add Order", "exp_report": "Export Report", "There_are_a_total_of": ",There are a total of", - "items": "items." + "items": "items.", + "order_detail": "Order Detail", + "back_to_list": "Back to List", + "order_not_found": "Order Not Found", + "order_info": "Order Information", + "cost_detail": "Cost Details", + "photo_remarks": "Photos", + "link": "Link", + "no_photos": "No photos", + "open_link": "Open Link", + "no_costs": "No cost records", + "cost_total": "Total", + "change_status": "Change Status", + "commit_history": "Status History", + "no_commits": "No status records", + "commit_placeholder": "Add comment (optional)", + "commit_create": "Order created" }, "purchase_addorder": { "add_order": "Add Order", diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index 7313d69..54620d4 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -66,6 +66,10 @@ "created_at": "创建日期", "updated_at": "更新日期", "status": "状态", + "status_pending": "待处理", + "status_ordered": "已下单", + "status_arrived": "已到达", + "status_received": "已收件", "completed": "已完成", "pending": "待处理", "show": "显示", @@ -74,7 +78,23 @@ "add_part": "添加订单", "exp_report": "生成报告", "There_are_a_total_of": ",一共", - "items": "个物件" + "items": "个物件", + "order_detail": "订单详情", + "back_to_list": "返回列表", + "order_not_found": "订单不存在", + "order_info": "订单信息", + "cost_detail": "费用明细", + "photo_remarks": "图片备注", + "link": "链接", + "no_photos": "暂无图片", + "open_link": "打开链接", + "no_costs": "暂无费用记录", + "cost_total": "合计", + "change_status": "变更状态", + "commit_history": "状态记录", + "no_commits": "暂无状态记录", + "commit_placeholder": "添加备注(可选)", + "commit_create": "订单创建" }, "purchase_addorder": { "add_order": "添加订单", diff --git a/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue b/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue index 27b2bec..ef5e5d7 100644 --- a/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue +++ b/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue @@ -122,14 +122,13 @@ onMounted(fetchOrders)
{{ $t('message.functionality_not_yet_developed') }}
+ + +{{ t('purchase.order_not_found') }}
+{{ order?.Title || '-' }}
+{{ order.Link }}
+ +-
+{{ order.Styles }}
+{{ order.Remark }}
+| {{ t('purchase_addorder.fee_type') }} | +{{ t('purchase_addorder.quantity') }} | +{{ t('purchase.unit_price') }} | +{{ t('purchase.total_price') }} | +{{ t('purchase_addorder.currency') }} | +
|---|---|---|---|---|
| + {{ costTypeMap[item.CostType] || item.CostType }} + | +{{ item.Quantity }} | +{{ formatPrice(item.Price) }} | ++ {{ formatPrice(item.Price * item.Quantity) }} + | ++ {{ currencyOptions[item.CurrencyType] || '-' }} + | +
| + {{ t('purchase.cost_total') }} + | ++ {{ costTotalYuan.toFixed(2) }} + | ++ + {{ g.currency }} {{ g.total }} + + | +||
+ {{ commit.Comment }} +
+