警告:如果这是一个文件夹,包括子文件夹和文档在内的所有内容将被删除。
为您的Wiki部分定义基于路径的访问规则。规则按顺序评估。首次匹配生效。
创建和管理您的 Wiki 数据备份。备份包括所有文档、图像和配置文件。
用途:这是"先想清楚再写代码"的定稿依据。写任何模块前先读本文件对应条目,
配合deploy/init.sql(表结构唯一权威)与docs/api-design.md(接口字段)一起用。
数据定义已定稿,按本文逻辑写,不要边写边发明规则。
handler(收参数/验格式/返回JSON)
→ service(业务规则/事务编排)
→ repository(SQL, 挂全局 Repo)
→ MySQL
tx := s.Repo.DB().Beginx() + defer tx.Rollback() + 成功 tx.Commit(){"code": 0, "msg": "ok", "data": {}} // code: 0成功, 400参数, 401未登录, 403无权限, 404不存在, 500服务器错, 1001+业务错
access_token(JWT,默认2小时,config jwt.access_expire)Authorization: Bearer <token>middleware.Auth 中间件:解析 token → 失败 401 → 成功把 userID 塞进 c.Set("uid", ...)| 接口 | 为什么必须事务 |
|---|---|
| 注册发新人礼包 | 建用户 + 加答题卡 + 加积分 + 记积分明细 |
| 答题 | 扣次数 + 记答题记录 + 加分 + 记积分明细 |
| 签到发奖 | 记签到记录 + 发答题卡/积分 |
| 邀请绑定 | 改被邀请人 inviter_id + 邀请人加邀请积分 + 记邀请记录 |
| 邀请积分换答题卡 | 扣邀请积分 + 加答题卡 + 记兑换记录 |
| 兑换处理(后台) | 扣积分 + 记积分明细 + 累计兑换 + 更新额度 |
| 钱包 | 字段 | 来源 | 用途 |
|---|---|---|---|
| 答题积分 points | users.points |
答对题/签到第7天(积分型)/广告额外积分/新人礼包 | 兑换现金(1积分=1元) |
| 答题卡 answer_cards | users.answer_cards |
新人礼包/签到/广告/邀请兑换 | 1张=1次答题机会 |
| 邀请积分 invite_points | users.invite_points |
邀请成功1人得若干 | 只用来兑换答题卡,不可换钱 |
累计字段:total_earned(累计获得答题积分,只增不减,排行榜用);total_redeemed(累计兑换掉的积分)。
| type | 含义 | change 正负 |
|---|---|---|
| 1 | 答题答对 | + |
| 2 | 签到第7天(积分型) | + |
| 3 | 新人礼包 | + |
| 4 | 看广告额外积分 | + |
| 5 | 兑换扣减 | - |
答题卡与邀请积分的变动没有明细表,只在 users 表记账(设计如此)。
daily_free_count(5)ad_reward_count(3答题卡) / ad_reward_points(0积分)register_gift_switch/cards/points(5卡+20分)signin_switch / signin_day_cards(1-6天) / signin_day7_kind(1卡2分) / signin_day7_cards(10) / signin_day7_points(50)invite_switch / invite_points_per_person(10) / invite_exchange_cost(30) / invite_exchange_cards(5) / invite_code_length(6)redeem_switch / min_redeem(10) / redeem_daily_limit / redeem_monthly_limit / redeem_max_once / customer_service_wechat / customer_service_qrcodeapp_name / welcome_text(维吾尔语)广告每日上限不在 sys_configs,在广告位配置
ad_configs.daily_limit(每个广告位自己的每日每人上限)。
铁律:不拆"注册""登录"两个接口,只有
POST /api/v1/auth/login。
MVP 微信未配置时前端直接传 openid;配好后前端传 code,后端用 pkg/wechat 换 openid。
POST /api/v1/auth/login
{"code": "wx_code", "openid": "test_001", "invite_code": "ABC123"} // code与openid二选一; invite_code可空
1. 取 openid: 传了 openid 直接用; 传了 code 调 wechat.Code2Session(code) 换 openid
2. repository 按 openid 查用户 GetUserByOpenID
3. 查到(老用户) → 跳过注册; 查不到(新用户) → 走注册分支 ↓
注册分支(事务):
a. 组装 user: nickname=随机"猜谜玩家_XXXXXX"(后端生成, 不引导填),
invite_code=随机大写字母+数字(长度读 invite_code_length), status=1
b. CreateUser 插入 → 拿自增ID
c. 新人礼包(register_gift_switch=1时): answer_cards += register_gift_cards
points += register_gift_points → 记 points_records(type=3, 新人礼包)
【调用已有 Repo.AddAnswerCards + Repo.AddPoints, 别重写】
d. 若带 invite_code → 走 6.1 邀请绑定(同事务, 奖励与礼包叠加)
4. 签发 token: jwt.GenerateToken(userID) → access_token
5. 返回 {user(脱敏: 无openid/unionid), access_token, is_new_user}
// TODO: 读 sys_configsGET /api/v1/user/profile(需登录) ¶handler: c.Get("uid") 拿 userID(中间件塞的) → service.GetUser → repo.GetUserByID
返回: user, 但 phone 脱敏(138****1234), openid/unionid 不返回
PUT /api/v1/user/profile(需登录) ¶请求: {"nickname":"新名","avatar":"url","phone":"138...","status":1} // 只传要改的
流程:
1. handler 绑定到 updateProfileReq(指针字段) → 只把非nil的装进 map
2. service.UpdateUserProfile(userID, map) → repo 动态拼SET + 白名单(nickname/avatar/phone/status)
3. openid/points/answer_cards 等敏感字段不在白名单 → 前端传了也被忽略
只能改自己的(uid 从 token 来); 后台改别人状态走管理接口
GET /api/v1/riddle/categories(无需登录) ¶repo: SELECT id, name, icon, sort_order FROM riddle_categories WHERE status=1 ORDER BY sort_order
返回: [{id, name, icon, sort_order}]
GET /api/v1/riddle/random?category_id=0(需登录) ¶取题 SQL 逻辑(排除今日已答):
SELECT * FROM riddles
WHERE status = 1
AND (category_id = ? OR ? = 0) -- 分类筛选, 0=全部
AND id NOT IN (
SELECT riddle_id FROM answer_records
WHERE user_id = ? AND DATE(created_at) = CURDATE() -- 今日已答的排除
)
ORDER BY RAND() LIMIT 1
service 返回前处理:
{id, question_type, title, question_image, options:[{key,text,image}], difficulty, points, hint}注意:options 里每个选项只有 key/text/image,没有"哪个是正确答案";正确答案只存表里,判题在后端。
POST /api/v1/game/answer(需登录) ¶{"riddle_id": 10, "selected_key": "B"}
1. 查谜语 GetRiddleByID(riddle_id) → 不存在/已禁用 → 报错
2. 校验 selected_key 在 options 的 key 列表里(防乱传)
3. 校验今日是否已答过此题 HasAnsweredToday(userID, riddleID) → 答过报错(防重复刷)
4. 校验并消耗次数(见 4.3)
5. 判题: isCorrect = (selected_key == riddle.answer)
6. 事务内:
a. 写 answer_records(user_id, riddle_id, selected_answer, is_correct,
points_earned, source) -- source: 1=消耗免费次数 2=消耗答题卡
b. 扣次数: 本次用的是免费次数 → daily_quota.used_count+1;
用的是答题卡 → users.answer_cards-1
c. 答对且 points>0 → users.points += riddle.points; total_earned += 同值
记 points_records(type=1 答题, change=+points) 【AddPoints 已封装】
7. 返回 {is_correct, answer_key(正确答案key), answer_text(正确选项文字/图URL, 展示用),
points_earned, hint}
1. 今日免费剩余>0 → 用免费次数(不花答题卡)
2. 否则 answer_cards>0 → 扣1张答题卡
3. 都没有 → 业务错误 "次数用尽, 请观看广告获取"(前端引导看广告)
daily_free_count;剩余 = 总数 - used_countGetOrCreateQuota(userID) —— 无今天记录则 INSERT 一行(used_count=0)GET /api/v1/game/quota(需登录) ¶返回: {free_total:5, free_remain:3, answer_cards:8,
ad_limit:广告位daily_limit, ad_remain:广告位daily_limit - daily_quota.ad_count}
规则:第 N 次签到 = 本轮第
N % 7天(余数0=第7天大奖日)。
昨天签了第1天、今天没签、明天签就是第2天——位置只由累计次数决定,与是否连续无关。
GET /api/v1/signin/info(需登录) ¶1. total_count = 已签到次数(COUNT signin_records 或记录里取)
2. day_no = total_count % 7, 余0则=7 (今天签了会是第几天)
round_no = (total_count-1)/7 + 1
3. 本轮起始天 start_day = (round_no-1)*7 + 1 (第1轮1-7, 第2轮8-14...)
4. signed_today = 今日是否已签
5. days: 本轮7个格子, 每格 {day_no, signed(该天序号<=已签到的位置), is_today, is_big_day(第7格), reward_text}
reward_text: 1-6天"+N答题卡"(N=signin_day_cards), 第7天按 kind 显示大奖
POST /api/v1/signin/check(需登录,事务) ¶1. signin_switch=0 → "签到已关闭"
2. 今日已签(HasSignedToday, uk_user_date兜底) → "今日已签到"
3. total = 已签到次数+1 → day_no/round_no 按上面公式
4. 算奖励:
day_no<7 → kind=1, value=signin_day_cards (答题卡)
day_no=7 → kind=signin_day7_kind
kind=1 → value=signin_day7_cards 加答题卡
kind=2 → value=signin_day7_points 加积分 + 记 points_records(type=2)
5. 事务:
a. INSERT signin_records(user_id, sign_date=CURDATE(), round_no, day_no,
total_count=total, reward_kind=kind, reward_value=value)
b. 发奖: kind=1 → users.answer_cards += value
kind=2 → users.points += value (AddPoints, type=2)
6. 返回 {round_no, day_no, total_count, reward_kind, reward_value}
POST /api/v1/invite/bind) ¶绑定时机:新用户首次注册时若带了 invite_code 自动绑定(在 Login 事务 d 步);
已注册用户也可走 bind 接口兜底(如通过海报进入)。一人只能被绑一次, 不能改绑。
流程(事务):
1. 校验: invite_code 非空; 不能绑自己(被绑人=邀请人 → 拒绝)
2. 查邀请人 GetUserByInviteCode(code) → 不存在/禁用 → 报错
3. 校验该被邀请人没绑过(users.inviter_id=0 或查 invite_records.uk_invitee) → 绑过报错
4. 事务:
a. users.inviter_id = 邀请人ID (被邀请人)
b. 邀请人 users.invite_points += invite_points_per_person(每邀请奖励)
c. INSERT invite_records(inviter_id, invitee_id, points=奖励值, status=1)
(uk_invitee 唯一, 数据库层兜底防重复)
GET /api/v1/invite/info(需登录) ¶返回: {invite_code(我的), invite_points, answer_cards,
per_person_points(配置m), exchange_cost(配置n), exchange_cards(配置o),
invite_count(我邀请了几人), invite_records:[{被邀请人昵称/头像/时间/积分}],
exchange_records:[{时间, 消耗积分, 获得卡片}]}
POST /api/v1/invite/exchange(需登录,事务) ¶1. invite_switch=0 → 关闭
2. 读配置 n=invite_exchange_cost, o=invite_exchange_cards
3. users.invite_points < n → "邀请积分不足"
4. 事务:
a. users.invite_points -= n
b. users.answer_cards += o
c. INSERT invite_exchange_records(user_id, cost_points=n, cards=o)
5. 返回新余额
GET /api/v1/redeem/info(需登录) ¶返回: {points(我的答题积分), min_redeem, redeem_switch,
customer_service_wechat, customer_service_qrcode,
daily_limit/monthly_limit/max_once(0=不限), frozen(额度表status=2=冻结)}
POST /api/v1/redeem/apply(需登录) ¶请求: {"points": 50, "contact": "微信号/备注"}
流程:
1. redeem_switch=0 → "兑换已关闭"
2. points < min_redeem → "未达最低兑换"
3. users.points < points → "积分不足"
4. 额度校验 CheckRedeemLimit(userID, points) (见 7.4)
-- 注意: 待处理申请(pending)占用的积分也要算进"已用额度"
5. INSERT redeem_records(user_id, points, contact, status=0待兑换)
6. webhook 提醒客服(以后做, 先留 TODO)
GET /api/v1/redeem/records?page=&size=(需登录) ¶分页返回 {list:[{points, status(0待兑换1已兑换2已取消), contact, remark, created_at}], total}
GetOrCreateRedeemLimit(userID): 无记录则按全局配置建默认行
检查顺序:
1. status=2 → "兑换已被冻结"
2. total_limit>0 且 used_total+points > total_limit → "累计额度超限"
3. 日计数重置: last_redeem_date != 今天 → daily_count=0 (同月逻辑重置 monthly_count)
4. daily_limit>0 且 daily_count+1 > daily_limit → "今日兑换次数/额度超限"
5. monthly_limit>0 且 monthly_count+1 > monthly_limit → "本月超限"
6. max_once>0 且 points > max_once → "单次超限"
PUT /admin/v1/redeems/:id ¶status 0→1 已兑换(客服已线下转账):
事务:
a. 校验申请还是"待兑换"(防重复处理, 用 UPDATE ... WHERE status=0 判断影响行数)
b. users.points -= 申请积分
记 points_records(type=5 兑换扣减, change=-points) ← 新定稿
c. users.total_redeemed += points(累计兑换, 只增)
d. redeem_records.status=1, operator_id=当前管理员
e. ConsumeRedeemLimit: used_total+=points, daily_count+1, monthly_count+1,
last_redeem_date=CURDATE()
status 0→2 已取消: 只改状态, 不扣积分不碰额度
⚠️ 微信激励视频无服务端验证接口,isEnded 来自客户端不可全信,后端必须自行风控。
GET /api/v1/ads?page=xxx&type=xxx(无需登录) ¶SELECT * FROM ad_configs WHERE status=1 [AND page=?] [AND ad_type=?] ORDER BY sort_order
返回: [{ad_unit_id, ad_type, position, daily_limit}] (前端广告管理器启动时拉)
POST /api/v1/ad/reward(需登录,事务) ¶请求: {"position": "game_quota"} // 广告场景
流程:
1. 查该广告位(GetAdByPosition: status=1, position匹配) → 无 → 报错
2. 风控(全部通过才发):
a. 今日已看次数 ad_count >= ad_configs.daily_limit → "今日广告次数已用完"
b. 距 last_ad_at < 30秒 → "操作太频繁"(防连点)
3. 事务(防重放: 读-校验-更新在事务内):
a. daily_quota.ad_count += 1; ad_reward_count += ad_reward_count配置值;
last_ad_at = NOW()
b. users.answer_cards += ad_reward_count(默认3)
c. ad_reward_points>0 → users.points += 它; 记 points_records(type=4 广告)
4. 返回 {cards: 本次答题卡, ad_remain: 今日剩余}
GET /api/v1/points/records?page=1&size=20(需登录) ¶SELECT change, balance, type, remark, created_at FROM points_records
WHERE user_id=? ORDER BY id DESC LIMIT ?,?
返回: {list:[...], total} // balance 是变动后余额(写入时快照)
GET /api/v1/rank/list?limit=100(需登录) ¶今日榜: SELECT user_id, SUM(points_earned) FROM answer_records
WHERE DATE(created_at)=CURDATE() GROUP BY user_id ORDER BY SUM DESC LIMIT ?
总榜: 按 users.total_earned 倒序 LIMIT ?
返回: [{rank, user_id, nickname, avatar, points}]
MVP 可先做"今日榜"(首页展示) + "总榜"两个模式(参数或分开接口, 定一个)
GET /api/v1/task/list(需登录) ¶任务不是数据库表,是 service 现算组装返回(任务中心页 4 个卡片):
1. 看广告得答题卡: {type:ad, 文案, 今日剩余: 广告位daily_limit-ad_count, enabled:广告位启用}
2. 每日答题: {type:answer, 今日答对X题进度(COUNT answer_records 今日答对), 目标按配置}
3. 签到领次数: {type:signin, 今日已签? enabled:signin_switch}
4. 邀请好友: {type:invite, 我邀请N人, 规则文案(m/n/o), enabled:invite_switch}
| 模块 | 关键逻辑 |
|---|---|
| 管理员登录 | POST /admin/v1/auth/login: username 查 admins → bcrypt 比对 password → 发 admin token(role 区分超管/普通) |
| 谜语管理 | CRUD riddles: 4种题型共用一张表; options 用 JSON 字段; 删除=软删 status=2; 图片传 COS |
| 分类管理 | CRUD riddle_categories |
| 用户管理 | 列表(关键词+分页) / PUT status(1正常2禁用) —— 禁用后其 token 应失效(中间件里查 status 可选做) |
| 兑换处理 | 见 7.5 |
| 兑换额度 | GET/PUT 每用户 redeem_limits(上限/冻结) |
| 系统配置 | GET/PUT sys_configs 任意项(奖励参数全在这) |
| 广告配置 | CRUD ad_configs(多类型多广告位, daily_limit 按广告位) |
| 邀请记录 | 查 invite_records / invite_exchange_records 列表 |
| 操作日志 | 后台所有写操作 → 写 admin_logs(admin_id, action, target_type/id, detail, ip) |
| 数据统计 | 各业务表聚合: users总数/今日新增、answer_records答题量、redeem_records待处理数等 |
1. 全局: pkg/response(统一返回) → 已完成的 JWT/middleware 收尾 → Login 一体化改造
2. 用户模块补齐: PointsRecord model; Register → Login(接 wechat+JWT); 随机昵称
3. 游戏模块: riddle/answer model → repository → service(4.2) → handler → router
4. 签到模块(五)
5. 邀请模块(六)
6. 兑换模块(七)
7. 广告模块(八)
8. 任务/排行/明细(九、十)
9. 管理后台(十一)
每完成一个模块:go build ./... + 按 api-design.md 该接口实测(Postman/Apifox 存集合),过了再进下一个。