From 9271f47553bcfec80270ceb1f735561b87487005 Mon Sep 17 00:00:00 2001 From: yml Date: Mon, 15 Jun 2026 20:37:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=85=AC=E5=91=8A=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=BC=96=E8=BE=91,=20=E5=A2=9E=E5=8A=A0=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/modules/announcement/handler.go | 16 + .../modules/announcement/repository.go | 11 + .../internal/modules/announcement/service.go | 5 + backend/internal/modules/file/handler.go | 2 +- backend/internal/modules/file/service.go | 4 +- backend/internal/modules/file/storage_test.go | 20 +- backend/internal/router/router.go | 1 + docs/拉卡拉.md | 318 ++++++++++++++++++ frontend/components.d.ts | 1 + .../features/admin/api/adminAnnouncements.ts | 4 + .../admin/components/AnnouncementDialog.vue | 300 ++++++++++++++++- .../admin/views/AdminAnnouncementsView.vue | 28 ++ .../features/announcement/utils/markdown.ts | 60 ++++ .../views/AnnouncementDetailView.vue | 10 +- .../views/MobileAnnouncementDetailView.vue | 9 +- frontend/src/shared/utils/imageUpload.ts | 2 + 16 files changed, 765 insertions(+), 26 deletions(-) create mode 100644 docs/拉卡拉.md create mode 100644 frontend/src/features/announcement/utils/markdown.ts diff --git a/backend/internal/modules/announcement/handler.go b/backend/internal/modules/announcement/handler.go index 6376a50..97b3f95 100644 --- a/backend/internal/modules/announcement/handler.go +++ b/backend/internal/modules/announcement/handler.go @@ -173,6 +173,22 @@ func (h *Handler) Archive(c *gin.Context) { response.OK(c, gin.H{"message": "归档成功"}) } +// Unarchive 取消归档公告 +func (h *Handler) Unarchive(c *gin.Context) { + id, err := parseID(c) + if err != nil { + response.BadRequest(c, "公告ID不正确") + return + } + + if err := h.service.Unarchive(c.Request.Context(), id); err != nil { + response.InternalServerError(c, "取消归档公告失败") + return + } + + response.OK(c, gin.H{"message": "取消归档成功"}) +} + // Delete 删除公告 func (h *Handler) Delete(c *gin.Context) { id, err := parseID(c) diff --git a/backend/internal/modules/announcement/repository.go b/backend/internal/modules/announcement/repository.go index ee0fa41..75a11c5 100644 --- a/backend/internal/modules/announcement/repository.go +++ b/backend/internal/modules/announcement/repository.go @@ -179,6 +179,17 @@ func (r *Repository) Archive(ctx context.Context, id uint64) error { Update("status", "archived").Error } +// Unarchive 取消归档公告,恢复为已发布状态 +func (r *Repository) Unarchive(ctx context.Context, id uint64) error { + now := time.Now() + return r.db.WithContext(ctx).Model(&model.Announcement{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "status": "published", + "published_at": gorm.Expr("COALESCE(published_at, ?)", now), + }).Error +} + // Delete 删除公告 func (r *Repository) Delete(ctx context.Context, id uint64) error { return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Announcement{}).Error diff --git a/backend/internal/modules/announcement/service.go b/backend/internal/modules/announcement/service.go index 921748d..2b381f1 100644 --- a/backend/internal/modules/announcement/service.go +++ b/backend/internal/modules/announcement/service.go @@ -95,6 +95,11 @@ func (s *Service) Archive(ctx context.Context, id uint64) error { return s.repo.Archive(ctx, id) } +// Unarchive 取消归档公告 +func (s *Service) Unarchive(ctx context.Context, id uint64) error { + return s.repo.Unarchive(ctx, id) +} + // Delete 删除公告 func (s *Service) Delete(ctx context.Context, id uint64) error { return s.repo.Delete(ctx, id) diff --git a/backend/internal/modules/file/handler.go b/backend/internal/modules/file/handler.go index fd1b2b6..aaa9263 100644 --- a/backend/internal/modules/file/handler.go +++ b/backend/internal/modules/file/handler.go @@ -65,7 +65,7 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) { response.BadRequest(c, "文件 key 不正确") return } - if publicOnly && !strings.HasPrefix(key, "home-banner/") && !strings.HasPrefix(key, "avatar/") && !strings.HasPrefix(key, "payment-cert/") { + if publicOnly && !strings.HasPrefix(key, "home-banner/") && !strings.HasPrefix(key, "avatar/") && !strings.HasPrefix(key, "payment-cert/") && !strings.HasPrefix(key, "announcement/") { response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问") return } diff --git a/backend/internal/modules/file/service.go b/backend/internal/modules/file/service.go index f0a9792..8df33b7 100644 --- a/backend/internal/modules/file/service.go +++ b/backend/internal/modules/file/service.go @@ -106,7 +106,7 @@ func normalizeContentType(contentType string, data []byte) string { func fileURLForScene(scene string, key string) string { fileURL := "/api/files/object?key=" + url.QueryEscape(key) - if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" { + if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" || scene == "announcement" { fileURL = "/api/public/files/object?key=" + url.QueryEscape(key) } return fileURL @@ -115,7 +115,7 @@ func fileURLForScene(scene string, key string) string { func normalizeScene(scene string) string { scene = strings.TrimSpace(strings.ToLower(scene)) switch scene { - case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert": + case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement": return scene default: return "misc" diff --git a/backend/internal/modules/file/storage_test.go b/backend/internal/modules/file/storage_test.go index d23ad2b..5fb103c 100644 --- a/backend/internal/modules/file/storage_test.go +++ b/backend/internal/modules/file/storage_test.go @@ -1,6 +1,9 @@ package file -import "testing" +import ( + "strings" + "testing" +) func TestSanitizeObjectMetadataEscapesNonASCII(t *testing.T) { got := sanitizeObjectMetadata(map[string]string{ @@ -11,3 +14,18 @@ func TestSanitizeObjectMetadataEscapesNonASCII(t *testing.T) { t.Fatalf("metadata filename = %q", got["original-filename"]) } } + +func TestAnnouncementSceneUsesPublicFileURL(t *testing.T) { + key := "announcement/2026/06/15/example.webp" + got := fileURLForScene("announcement", key) + + if !strings.HasPrefix(got, "/api/public/files/object?") { + t.Fatalf("announcement file url = %q, want public file endpoint", got) + } +} + +func TestNormalizeSceneAllowsAnnouncement(t *testing.T) { + if got := normalizeScene("announcement"); got != "announcement" { + t.Fatalf("normalize announcement scene = %q", got) + } +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index bda04a2..1cc4903 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -570,6 +570,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { adminRoutes.PUT("/announcements/:id", requirePerm("announcement:manage"), announcementHandler.Update) adminRoutes.POST("/announcements/:id/publish", requirePerm("announcement:manage"), announcementHandler.Publish) adminRoutes.POST("/announcements/:id/archive", requirePerm("announcement:manage"), announcementHandler.Archive) + adminRoutes.POST("/announcements/:id/unarchive", requirePerm("announcement:manage"), announcementHandler.Unarchive) adminRoutes.DELETE("/announcements/:id", requirePerm("announcement:manage"), announcementHandler.Delete) } } diff --git a/docs/拉卡拉.md b/docs/拉卡拉.md new file mode 100644 index 0000000..160d6d9 --- /dev/null +++ b/docs/拉卡拉.md @@ -0,0 +1,318 @@ +支付方式 +聚合支付 +聚合收银台 +收银台订单创建 +收银台订单创建 +更新时间:2026-04-28 17:42:56 +调用地址 +注:因银网联测试环境问题,微信钱包在测试环境下无法下单完成支付,下单后提示"sub mch id与sub appid不匹配"报错即可。 + +自2024年1月16日起,请通过以下接口进行接入: + +使用HTTP协议,POST方式提交。 + +URL(测试环境外网):https://test.wsmsd.cn/sit/api/v3/ccss/counter/order/special_create + +URL(生产环境):https://s2.lakala.com/api/v3/ccss/counter/order/special_create + +商户需在微信商家后台配置以下支付域名(原支付目录),2023 年 9 月后入网的商户可忽略此配置: + +生产环境订单域名:pay.lakala.com测试环境订单域名:pay.wsmsd.cn + +请求参数 +字段名 是否必输 类型 长度 字段描述 示例 +out_order_no M String 32 商户订单号 12345678 +merchant_no M String 32 银联商户号 822100041120005 +vpos_id C String 32 交易设备标识,进件返回接口中的termId字段,非API接口进件请联系业务员。 462621830268882944 +channel_id C String 32 渠道号 (一般不用) 24865454154 +total_amount M long 12 订单金额,单位:分 200 +order_efficient_time M String 14 订单有效期 格式yyyyMMddHHmmss,最大支持下单时间+7天 20210803141700 +notify_url C String 128 订单支付成功后商户接收订单通知的地址 http://xxx.xxx.com +support_cancel C int 1 是否支持撤销 默认 0 不支持 +busi_mode为"PAY-付款"不支持 撤销 (0 不支持 1支持) +support_refund C int 1 是否支持退款 默认0 不支持 (0 不支持 1支持) +support_repeat_pay C int 1 是否支持"多次发起支付" 默认0 不支持 (0 不支持 1支持) +out_user_id C String 64 发起订单方的userId,归属于channelId下的userId +callback_url C String 128 客户端下单完成支付后返回的商户网页跳转地址。 +order_info M String 64 +订单标题,在使用收银台扫码支付时必输入,交易时送往账户端 + +*该字段暂不支持颜文字、表情符号,比如微笑表情等 + + +term_no C String 32 结算终端号,合单场景必输该字段 +split_mark C String 2 合单标识,"1"为合单,不填默认是为非合单 +settle_type C String 4 +结算类型(非合单) "0"或者空,常规结算方式;如需接拉卡拉分账通需传"1",商户未开通分账之前切记不用上送此参数,如需订单结算需传"2" + + +out_split_info C List<> +拆单信息 +合单标识为"1"时必传该字段。,详细字段见out_split_info字段说明 +counter_param C String 1024 json字符串 收银台展示参数 {\"pay_mode\" : \"ALIPAY\"} ,指定支付方式为支付宝 +ALIPAY:支付宝 +WECHAT:微信 +UNION:银联云闪付 +CARD:POS刷卡交易 +LKLAT:线上转帐 +QUICK_PAY:快捷支付 +EBANK:网银支付 +UNION_CC:银联支付 +BESTPAY:翼支付 +HB_FQ:花呗分期 +UNION_FQ:银联聚分期 + +ONLINE_CARDLESS:线上外卡 + +JDBT:京东白条 + +ALIPAY_HK:支付宝香港钱包支付 + +若要指定支付方式为支付宝传参格式: +{\"pay_mode\" : \"ALIPAY\"} + +counter_remark +C String 128 收银台备注 +busi_type_param C String 256 业务类型控制参数,jsonStr格式 [{\"busi_type\":\"UPCARD\",\"params\":{\"crd_flg\":\"CRDFLG_D|CRDFLG_C|CRDFLG_OTH\"}},{\"busi_type\":\"SCPAY\",\"params\":{\"pay_mode\":\"ALIPAY\",\"crd_flg\":\"CRDFLG_D\"}}] +说明:UPCARD-刷卡,SCPAY-扫码,CRDFLG_D-借记卡,CRDFLG_C-贷记卡,CRDFLG_OTH-不明确是借记卡还是贷记卡 +pay_mode送参说明:ALIPAY-支付宝,WECHAT-微信,UNION-银联二维码,DCPAY-数字货币,BESTPAY-翼支付 +说明:一旦使用该字段,则增加限制,必须在指定限制范围内支付。比如,只配置"busi_type":"UPCARD"的参数而不配置"busi_type":"SCPAY"的参数,则只能通过刷卡而不能通过扫码完成支付 + +sgn_info C list<> +签约协议号列表(字符串) ["1234","2345"],不支持空列表[];列表中签约协议号不能为空;列表中签约协议号不能重复 +product_id +C String 6 指定产品编号 (200809:线上外卡收银台) 注意:该字段默认不需要指定,特殊场景下使用,慎用 +goods_mark C String +商品信息标识 (1:含商品信息,不填默认不含商品信息) +goods_field C String 2 商品信息域(good_mark送1时该域必填,否则不送。只有线上外卡业务上送该字段) 详细字段见goods_field字段说明 +order_scene_field C Object +2 订单场景域,特殊场景下需要上送 详细字段见order_scene_field字段说明 +age_limit C String 1 0:不限年龄;1:年龄限制 +repeat_pay_auto_refund C String 1 +0:重复支付后不自动退货;1:重复支付后自动退货 (默认不送为0),注意:请详细了解字段场景后上送 + +需注意互斥条件:repeat_pay_auto_refund选择"1"重复支付后自动退货后,repeat_pay_notify仅支持选择"0"重复支付订单不通知 + + +repeat_pay_notify C String 1 0:重复支付订单不通知;1:重复支付订单通知 (默认不送为0) +close_order_auto_refund C String 1 0:不自动退货;1:关闭订单后支付成功触发自动退货 (默认不送为0)注意:请详细了解字段场景后上送 +shop_name C String 64 网点名称 +inte_routing C String 2 智能路由下单标识 1-是 0-否(默认不送为0)备注:需要在收银台管控台配置聚合收银台小程序白名单 +discount_code C String 64 优惠码(目前供线上国补下单使用) +electrical_equipment_category C +String +128 支付宝优惠码(目前优惠码的地区:浙江、江苏、上海、福建、重庆) +trade_biz_tp C String 16 线上业务通道类型 +具体类型见补充枚举 + +* 网银支付、快捷支付必传此字段 + +cash_pay_show_mode C +String 32 对公支付标识 LQF-对公支付 +out_split_info字段说明 +字段名 中文名称 是否必填 类型 说明 +out_sub_order_no 外部子订单号 M String(32) 商户子订单号 +merchant_no 商户号 M String(32) 拉卡拉分配的银联商户号 +term_no 终端号 M String(32) 拉卡拉分配的业务终端号 +amount 金额 M String(12) 单位分,整数型字符 +settle_type 结算类型(合单) C String(4) "0"或者空,常规结算方式 +说明: + + 1)拆单信息域中商户号不可重复; + + 2)交易层订单金额必须是拆单信息域中各个子单的金额汇总之和; + + 3)对拆单信息域中每个结算商户号和终端号的权限交易都必须通过,其中一个校验失败,则交易中止,失败返回; + + 4)拆单域中子单条数最少两条、最多50条,否则拒绝。 + +goods_field字段说明 +字段名 中文名称 是否必填 类型 说明 +goods_amt 商品单价 M Long 单位:分 +goods_num 商品数量 M Integer +goods_pricing_unit 商品计价单位 M String(8) 1-箱 2-件 3-瓶 4-个 +goods_name 商品名称 M String(128) +te_platform_type 交易电商平台类型 M String(2) 1-境内平台 2-境外平台 +te_platform_name 交易电商平台名称 M String(256) +goods_type 交易商品类型 M String(8) 1:服饰箱包 +2:食品药品 +3:化妆品 +4:电子产品 +5:日用家居 +7:航空机票 +8:酒店住宿 +9:留学教育 +10:旅游票务 +11:国际物流 +12:国际租车 +13:国际会议 +14:软件服务 +15:医疗服务 +16:通讯 +17:休闲娱乐 +order_scene_field字段说明 +字段名 中文名称 是否必填 类型 说明 +order_scene_type 订单场景类型 M String(16) 订单场景类型(按下述定义场景送值) +HB_FQ:花呗分期场景 + +KL_FQ:考拉分期场景 + +scene_info 订单场景信息 C String(1024) 订单场景信息(json字符串格式),不同的订单场景类型需要上送的结构不一样(详见具体场景) +HB_FQ场景 +scene_info字段说明 +字段名 中文名称 是否必填 类型 说明 +hbFqNum 花呗分期期数 M String 支付宝花呗分期必送字段: 花呗分期数 3:3期 6:6期 12:12期 +hbFqSellerPercent 卖家承担手续费比例 M String 支付宝花呗分期必送字段: 卖家承担手续费比例,间连模式下只支持传0。 +JDBT场景 +scene_info字段说明 +字段名 中文名称 是否必填 类型 说明 +LOCKPLAN + +String +->jdbtFqNum 京东白条分期期数 M String 京东白条分期数 3:3期,6:6期,12:12期 ,24:24期 +trade_biz_tp字段说明 +拉卡拉业务种类编码 编码含义 +100001 虚拟商品购买 +100002 预付费类账户充值 +100003 实物消费 +100004 航空商旅消费 +100005 生活及商业服务消费 +100006 其他商户消费 +100007 招投标保证金支付 +100008 境外商品购买 +100A01 实物商品租赁 +100A03 单用途预付卡充值 +100A06 商业服务消费 +100A05 航旅交通服务 +100A07 生活服务消费 +100A08 个人经营服务 +110001 公共事业缴费 +110002 教育医疗缴费 +110003 政府服务缴费 +110004 公益捐款 +110005 农林牧副渔收购 +110006 政府服务 +110007 薪资发放 +110008 其他公共服务 +110A01 水电煤气缴费 +110A02 税费缴纳 +110A03 非营利性教育缴费 +110A05 罚款缴纳 +110A06 路桥通行缴费 +110A07 邮政缴费 +110A08 电视账单缴费 +110A09 话费账单缴费 +110A10 宽带账单缴费 +110A13 财政非税收入 +110A14 营利性教育培训 +110A15 公共交通 +110A16 急救救援 +110A17 物业缴费 +110A18 国库经收 +110A19 供暖费缴纳 +110A20 废弃物处理费用缴纳 +110A21 租金缴纳 +110A22 会员费用缴纳 +110A23 税费退还 +120001 其他金融付款 +120002 其他金融收款 +120003 基金购买 +120004 保险选购 +120005 投资理财 +120006 信贷偿还 +120007 信用卡还款转出 +120008 基金赎回/返还/分红 +120009 保险理赔/分红 +120010 投资理财赎回/返还/分红 +120011 信贷发放 +120012 信用卡还款转入 +120A01 基金理财产品申购 +120A02 基金理财产品认购 +120A03 非投资型保险费用缴纳 +120A05 商业众筹 +120A06 贵金属投资买入 +120A07 基金理财产品赎回 +120A08 基金理财产品到期返还 +120A09 认/申购失败返还 +120A10 基金理财产品分红 +120A11 保险理赔或退费 +120A12 保险红利发放或给付发放 +120A13 贵金属投资买出 +120A16 融资租赁 +120A19 投资型保险费用缴纳 +120A20 小贷公司贷款还款 +120A21 保单贷款发放 +120A22 其他保险资金代发 +130001 支付账户充值 +130002 支付账户回提 +130003 银行账户转账转出 +130004 其他账户充值 +130005 银行账户转账转入 +130006 其他账户回提 +130A03 向他人支付账户转账 +130A05 预付卡赎回 -个人赎回 +130A06 预付卡赎回-单位赎回 +130A08 测试验证资金 +130A09 薪酬福利发放 +130A10 代发货款 +140001 商户结算-交易资金结算 +140002 营销返现 +140003 其他商户结算 +140A02 预付卡商户结算 +140A03 商户收单资金提现 +150A01 资金归集 +200000 对公业务 + + +请求样例 +{ + "req_data": { + "out_order_no": "KFPT20220714160009228907288", + "merchant_no": "8222900701106PZ", + "vpos_id": "587305941625155584", + "channel_id": "2021052614391", + "total_amount": "1", + "order_efficient_time": "20220714170009", + "notify_url": "http://run.mocky.io/v3/b02c9448-20a2-4ff6-a678-38ecab30161d", + "support_cancel": "0", + "support_refund": "1", + "support_repeat_pay": "1", + "busi_type_param": "[{\"busi_type\":\"UPCARD\",\"params\":{\"crd_flg\":\"CRDFLG_D|CRDFLG_C|CRDFLG_OTH\"}},{\"busi_type\":\"SCPAY\",\"params\":{\"pay_mode\":\"WECHAT\",\"crd_flg\":\"CRDFLG_D\"}}]", + "counter_param": "{\"pay_mode\":\"ALIPAY\"}", + "out_user_id": "", + "order_info": "自动化测试", + "extend_info": "自动化测试", + "callback_url": "" + }, + "version": "3.0", + "req_time": "20220714160009"} +返回参数 +字段名 是否必输 类型 长度 字段描述 示例 +merchant_no M String 32 +银联商户号 +channel_id M String 32 + +out_order_no M String 32 商户订单号 +order_create_time M String 32 创建订单时间 订单系统创建订单的时间,格式yyyyMMddHHmmss +order_efficient_time M String 32 订单有效截至时间 格式yyyyMMddHHmmss +pay_order_no M String 64 平台订单号 21070211012001970631000383039 +total_amount M long 12 订单金额,单位:分 200 +counter_url M String 256 收银台地址信息 +响应样例 +{ + "msg": "操作成功", + "resp_time": "20210922181057", + "code": "000000", + "resp_data": { + "merchant_no": "8222900701106PZ", + "channel_id": "25", + "out_order_no": "KFPT20220714160009228907288", + "order_create_time": "20210922181056", + "order_efficient_time": "20210803141700", + "pay_order_no": "21092211012001970631000488056", + "counter_url": "http://q.huijingcai.top/b/pay?merchantNo=8221210594300JY&merchantOrderNo=08F4542EEC6A4497BC419161747A92FQ&payOrderNo=21092211012001970631000488056" + } +} +返回码code一览表 +msg code +成功 000000 diff --git a/frontend/components.d.ts b/frontend/components.d.ts index dcc77e4..1bbeec5 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -49,6 +49,7 @@ declare module 'vue' { ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSlider: typeof import('element-plus/es')['ElSlider'] ElStep: typeof import('element-plus/es')['ElStep'] ElSteps: typeof import('element-plus/es')['ElSteps'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] diff --git a/frontend/src/features/admin/api/adminAnnouncements.ts b/frontend/src/features/admin/api/adminAnnouncements.ts index 4c7c774..414cb61 100644 --- a/frontend/src/features/admin/api/adminAnnouncements.ts +++ b/frontend/src/features/admin/api/adminAnnouncements.ts @@ -59,6 +59,10 @@ export async function archiveAnnouncement(id: number): Promise { await apiClient.post(`/admin/announcements/${id}/archive`) } +export async function unarchiveAnnouncement(id: number): Promise { + await apiClient.post(`/admin/announcements/${id}/unarchive`) +} + export async function deleteAnnouncement(id: number): Promise { await apiClient.delete(`/admin/announcements/${id}`) } diff --git a/frontend/src/features/admin/components/AnnouncementDialog.vue b/frontend/src/features/admin/components/AnnouncementDialog.vue index 6f85e6d..771650d 100644 --- a/frontend/src/features/admin/components/AnnouncementDialog.vue +++ b/frontend/src/features/admin/components/AnnouncementDialog.vue @@ -1,13 +1,16 @@ + + +
+ +
+ + + + + +
+ + + +
+
+ +
+ + + + 原宽 +
+
+
+ +